filter_param 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE +21 -0
  4. data/README.md +138 -0
  5. data/lib/filter_param/ast/attribute.rb +15 -0
  6. data/lib/filter_param/ast/expressions.rb +37 -0
  7. data/lib/filter_param/ast/group.rb +13 -0
  8. data/lib/filter_param/ast/literal.rb +26 -0
  9. data/lib/filter_param/ast/literals/boolean.rb +38 -0
  10. data/lib/filter_param/ast/literals/date.rb +33 -0
  11. data/lib/filter_param/ast/literals/date_time.rb +36 -0
  12. data/lib/filter_param/ast/literals/decimal.rb +29 -0
  13. data/lib/filter_param/ast/literals/integer.rb +39 -0
  14. data/lib/filter_param/ast/literals/null.rb +19 -0
  15. data/lib/filter_param/ast/literals/string.rb +43 -0
  16. data/lib/filter_param/ast/node.rb +15 -0
  17. data/lib/filter_param/ast/scope.rb +12 -0
  18. data/lib/filter_param/definition.rb +157 -0
  19. data/lib/filter_param/field.rb +45 -0
  20. data/lib/filter_param/operator.rb +46 -0
  21. data/lib/filter_param/operators/and.rb +13 -0
  22. data/lib/filter_param/operators/case_insensitive_equal.rb +16 -0
  23. data/lib/filter_param/operators/contains.rb +18 -0
  24. data/lib/filter_param/operators/ends_with.rb +18 -0
  25. data/lib/filter_param/operators/equal.rb +21 -0
  26. data/lib/filter_param/operators/field_filter_operator.rb +40 -0
  27. data/lib/filter_param/operators/greater_than.rb +16 -0
  28. data/lib/filter_param/operators/greater_than_equal.rb +16 -0
  29. data/lib/filter_param/operators/group.rb +17 -0
  30. data/lib/filter_param/operators/less_than.rb +16 -0
  31. data/lib/filter_param/operators/less_than_equal.rb +16 -0
  32. data/lib/filter_param/operators/not.rb +16 -0
  33. data/lib/filter_param/operators/not_equal.rb +19 -0
  34. data/lib/filter_param/operators/or.rb +13 -0
  35. data/lib/filter_param/operators/present.rb +21 -0
  36. data/lib/filter_param/operators/starts_with.rb +18 -0
  37. data/lib/filter_param/parser.rb +144 -0
  38. data/lib/filter_param/scope.rb +24 -0
  39. data/lib/filter_param/transformer.rb +37 -0
  40. data/lib/filter_param/transpiler.rb +114 -0
  41. data/lib/filter_param/version.rb +5 -0
  42. data/lib/filter_param.rb +70 -0
  43. metadata +143 -0
@@ -0,0 +1,45 @@
1
+ module FilterParam
2
+ class Field
3
+ TYPES = %i[boolean string integer decimal date datetime].freeze
4
+
5
+ attr_reader :type, :name
6
+
7
+ def initialize(name, type, options = {})
8
+ @name = name
9
+ @type = field_type(type)
10
+ @rename = field_rename(options[:rename])
11
+ @value_transformer = options[:value]
12
+ end
13
+
14
+ def transform_value(value)
15
+ return value if value_transformer.blank?
16
+
17
+ value_transformer.call(value)
18
+ end
19
+
20
+ def actual_name
21
+ rename.presence || name
22
+ end
23
+
24
+ def allow_operator?(operator)
25
+ true
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :value_transformer, :rename
31
+
32
+ def field_rename(rename)
33
+ return rename.call(name) if rename.is_a?(Proc)
34
+
35
+ rename
36
+ end
37
+
38
+ def field_type(type)
39
+ type = (type.presence || :string).to_sym
40
+ return type if type.in?(TYPES)
41
+
42
+ raise UnknownType.new("Unknown type '#{type}' for field '#{name}'. Allowed types: #{TYPES}.")
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,46 @@
1
+ module FilterParam
2
+ class Operator
3
+ class << self
4
+ def type
5
+ return :unary if method(:sql).arity == 1
6
+
7
+ :binary
8
+ end
9
+
10
+ def operator_tag(operator_tag)
11
+ @operator_tag ||= operator_tag
12
+ end
13
+
14
+ def register(operator_clazz)
15
+ operator_tag = operator_clazz.tag
16
+ registry[operator_tag] = operator_clazz
17
+ end
18
+
19
+ def tag
20
+ @operator_tag
21
+ end
22
+
23
+ def for(operator_tag)
24
+ registry[operator_tag]
25
+ end
26
+
27
+ def binaries
28
+ with_type(:binary).map(&:tag)
29
+ end
30
+
31
+ def unaries
32
+ with_type(:unary).map(&:tag)
33
+ end
34
+
35
+ private
36
+
37
+ def with_type(type)
38
+ registry.values.select { |op| op < self && op.type == type }
39
+ end
40
+
41
+ def registry
42
+ @@registry ||= {}
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,13 @@
1
+ module FilterParam
2
+ module Operators
3
+ class And < Operator
4
+ operator_tag :and
5
+
6
+ def self.sql(left, right)
7
+ "#{left} AND #{right}"
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ FilterParam::Operator.register(FilterParam::Operators::And)
@@ -0,0 +1,16 @@
1
+ module FilterParam
2
+ module Operators
3
+ class CaseInsensitiveEqual < FieldFilterOperator
4
+ operator_tag :eq_ci
5
+ operand_data_type :string
6
+
7
+ def self.sql(field, literal)
8
+ super
9
+
10
+ "lower(#{field.actual_name}) = #{sql_quote(literal.value.downcase)}"
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ FilterParam::Operator.register(FilterParam::Operators::CaseInsensitiveEqual)
@@ -0,0 +1,18 @@
1
+ module FilterParam
2
+ module Operators
3
+ class Contains < FieldFilterOperator
4
+ operator_tag :co
5
+ operand_data_type :string
6
+
7
+ def self.sql(field, literal)
8
+ super
9
+
10
+ pattern = "%#{literal.value}%"
11
+
12
+ "#{field.actual_name} LIKE #{sql_quote(pattern)}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ FilterParam::Operator.register(FilterParam::Operators::Contains)
@@ -0,0 +1,18 @@
1
+ module FilterParam
2
+ module Operators
3
+ class EndsWith < FieldFilterOperator
4
+ operator_tag :ew
5
+ operand_data_type :string
6
+
7
+ def self.sql(field, literal)
8
+ super
9
+
10
+ pattern = "%#{literal.value}"
11
+
12
+ "#{field.actual_name} LIKE #{sql_quote(pattern)}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ FilterParam::Operator.register(FilterParam::Operators::EndsWith)
@@ -0,0 +1,21 @@
1
+ module FilterParam
2
+ module Operators
3
+ class Equal < FieldFilterOperator
4
+ operator_tag :eq
5
+
6
+ def self.sql(field, literal)
7
+ super
8
+
9
+ return "#{field.actual_name} IS NULL" if literal.value.nil?
10
+
11
+ "#{field.actual_name} = #{sql_quote(literal.value)}"
12
+ end
13
+
14
+ def self.negated_sql(field, literal)
15
+ Operators::NotEqual.sql(field, literal)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ FilterParam::Operator.register(FilterParam::Operators::Equal)
@@ -0,0 +1,40 @@
1
+ module FilterParam
2
+ module Operators
3
+ class FieldFilterOperator < Operator
4
+ class << self
5
+ attr_reader :operand_data_types
6
+
7
+ def operand_data_type(*data_types)
8
+ @operand_data_types ||= Set.new
9
+ @operand_data_types.merge(data_types)
10
+ @operand_data_types
11
+ end
12
+
13
+ def sql(field, literal)
14
+ validate_field!(field)
15
+ validate_literal!(literal)
16
+ end
17
+
18
+ private
19
+
20
+ def validate_field!(field)
21
+ field.allow_operator?(tag)
22
+ end
23
+
24
+ def validate_literal!(literal)
25
+ return if literal.nil?
26
+ return if operand_data_types.nil?
27
+ return if literal.data_type.in?(operand_data_types)
28
+
29
+ raise FilterParam::InvalidLiteral.new(
30
+ "Unexpected #{literal.data_type} operand for operator '#{tag}'."
31
+ )
32
+ end
33
+
34
+ def sql_quote(value)
35
+ ActiveRecord::Base.connection.quote(value)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,16 @@
1
+ module FilterParam
2
+ module Operators
3
+ class GreaterThan < FieldFilterOperator
4
+ operator_tag :gt
5
+ operand_data_type :string, :integer, :decimal, :date, :datetime
6
+
7
+ def self.sql(field, literal)
8
+ super
9
+
10
+ "#{field.actual_name} > #{sql_quote(literal.value)}"
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ FilterParam::Operator.register(FilterParam::Operators::GreaterThan)
@@ -0,0 +1,16 @@
1
+ module FilterParam
2
+ module Operators
3
+ class GreaterThanEqual < FieldFilterOperator
4
+ operator_tag :ge
5
+ operand_data_type :string, :integer, :decimal, :date, :datetime
6
+
7
+ def self.sql(field, literal)
8
+ super
9
+
10
+ "#{field.actual_name} >= #{sql_quote(literal.value)}"
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ FilterParam::Operator.register(FilterParam::Operators::GreaterThanEqual)
@@ -0,0 +1,17 @@
1
+ module FilterParam
2
+ module Operators
3
+ class Group < Operator
4
+ operator_tag :group
5
+
6
+ def self.sql(expression)
7
+ "(#{expression})"
8
+ end
9
+
10
+ def self.negated_sql(expression)
11
+ "NOT (#{expression})"
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ FilterParam::Operator.register(FilterParam::Operators::Group)
@@ -0,0 +1,16 @@
1
+ module FilterParam
2
+ module Operators
3
+ class LessThan < FieldFilterOperator
4
+ operator_tag :lt
5
+ operand_data_type :string, :integer, :decimal, :date, :datetime
6
+
7
+ def self.sql(field, literal)
8
+ super
9
+
10
+ "#{field.actual_name} < #{sql_quote(literal.value)}"
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ FilterParam::Operator.register(FilterParam::Operators::LessThan)
@@ -0,0 +1,16 @@
1
+ module FilterParam
2
+ module Operators
3
+ class LessThanEqual < FieldFilterOperator
4
+ operator_tag :le
5
+ operand_data_type :string, :integer, :decimal, :date, :datetime
6
+
7
+ def self.sql(field, literal)
8
+ super
9
+
10
+ "#{field.actual_name} <= #{sql_quote(literal.value)}"
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ FilterParam::Operator.register(FilterParam::Operators::LessThanEqual)
@@ -0,0 +1,16 @@
1
+ module FilterParam
2
+ module Operators
3
+ class Not < Operator
4
+ operator_tag :not
5
+
6
+ def self.sql(expression_operator, *expression_operands)
7
+ operator = Operator.for(expression_operator)
8
+ return operator.negated_sql(*expression_operands) if operator.respond_to?(:negated_sql)
9
+
10
+ "NOT #{operator.sql(*expression_operands)}"
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ FilterParam::Operator.register(FilterParam::Operators::Not)
@@ -0,0 +1,19 @@
1
+ module FilterParam
2
+ module Operators
3
+ class NotEqual < FieldFilterOperator
4
+ operator_tag :neq
5
+
6
+ def self.sql(field, literal)
7
+ return "#{field.actual_name} IS NOT NULL" if literal.value.nil?
8
+
9
+ "#{field.actual_name} != #{sql_quote(literal.value)}"
10
+ end
11
+
12
+ def self.negated_sql(field, literal)
13
+ Operators::Equal.sql(field, literal)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ FilterParam::Operator.register(FilterParam::Operators::NotEqual)
@@ -0,0 +1,13 @@
1
+ module FilterParam
2
+ module Operators
3
+ class Or < Operator
4
+ operator_tag :or
5
+
6
+ def self.sql(left, right)
7
+ "#{left} OR #{right}"
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ FilterParam::Operator.register(FilterParam::Operators::Or)
@@ -0,0 +1,21 @@
1
+ module FilterParam
2
+ module Operators
3
+ class Present < FieldFilterOperator
4
+ operator_tag :pr
5
+
6
+ def self.sql(field)
7
+ return "#{field.actual_name} IS NOT NULL" unless field.type == :string
8
+
9
+ "(#{field.actual_name} IS NOT NULL AND TRIM(#{field.actual_name}) != '')"
10
+ end
11
+
12
+ def self.negated_sql(field)
13
+ return "#{field.actual_name} IS NULL" unless field.type == :string
14
+
15
+ "(#{field.actual_name} IS NULL OR TRIM(#{field.actual_name}) = '')"
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ FilterParam::Operator.register(FilterParam::Operators::Present)
@@ -0,0 +1,18 @@
1
+ module FilterParam
2
+ module Operators
3
+ class StartsWith < FieldFilterOperator
4
+ operator_tag :sw
5
+ operand_data_type :string
6
+
7
+ def self.sql(field, literal)
8
+ super
9
+
10
+ pattern = "#{literal.value}%"
11
+
12
+ "#{field.actual_name} LIKE #{sql_quote(pattern)}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ FilterParam::Operator.register(FilterParam::Operators::StartsWith)
@@ -0,0 +1,144 @@
1
+ module FilterParam
2
+ class Parser < Parslet::Parser
3
+ rule(:space) { match("\s").repeat(1).ignore }
4
+ rule(:space?) { space.maybe }
5
+ rule(:dot) { str(".") }
6
+ rule(:lparen) { str("(") }
7
+ rule(:rparen) { str(")") }
8
+ rule(:escape_seq) { str('\\').present? >> str('\\') >> any }
9
+ rule(:double_quote) { str('"') }
10
+ rule(:single_quote) { str("'") }
11
+ rule(:digit) { match("[0-9]") }
12
+ rule(:zero) { str("0") }
13
+ rule(:non_zero_digit) { match("[1-9]") }
14
+ rule(:sig_number) { non_zero_digit >> digit.repeat(1).maybe }
15
+ rule(:zero_nonsig) { zero.repeat(0).maybe.ignore }
16
+ rule(:hyphen) { str("-") }
17
+ rule(:identifier) { match("[a-zA-Z_]") >> digit.maybe }
18
+ rule(:table) { identifier.repeat(1) >> dot }
19
+ rule(:attribute) { (table.maybe >> identifier.repeat(1)).as(:attribute) }
20
+ rule(:scope_name) { identifier.repeat(1) }
21
+
22
+ # Literals / types
23
+ rule(:null) { str("null").as(:null) }
24
+ rule(:boolean) { (str("true") | str("false")).as(:boolean) }
25
+
26
+ rule(:integer) do
27
+ (
28
+ (hyphen.maybe >> zero_nonsig >> sig_number) |
29
+ (hyphen.maybe.ignore >> zero >> zero_nonsig)
30
+ ).as(:integer)
31
+ end
32
+
33
+ rule(:decimal) do
34
+ (
35
+ (hyphen.maybe >> zero_nonsig >> sig_number >> dot >> digit.repeat(1)) |
36
+ (hyphen.maybe >> zero >> zero_nonsig >> dot >> digit.repeat(1))
37
+ ).as(:decimal)
38
+ end
39
+
40
+ rule(:string) do
41
+ (single_quote >> (escape_seq | match("[^\']")).repeat.as(:string) >> single_quote) |
42
+ (double_quote >> (escape_seq | match("[^\"]")).repeat.as(:string) >> double_quote)
43
+ end
44
+ rule(:date_yyyy) { digit.repeat(4) }
45
+ rule(:date_mm) { (zero >> non_zero_digit) | (str("1") >> match("[0-2]")) }
46
+ rule(:date_md) { (zero >> non_zero_digit) | (match("[1-2]") >> digit) | (str("3") >> match("[0-1]")) }
47
+ rule(:date_iso8601) { date_yyyy >> hyphen >> date_mm >> hyphen >> date_md }
48
+ rule(:date) { quoted date_iso8601.as(:date) }
49
+ rule(:time_hh_mi) do
50
+ (((zero | str("1")) >> digit) | (str("2") >> match("[0-3]"))) >>
51
+ str(":").maybe >> ((zero >> digit) | (match("[1-5]") >> digit))
52
+ end
53
+ rule(:time_hh_mi_ss) { time_hh_mi >> str(":") >> ((zero >> digit) | (match("[1-5]") >> digit)) }
54
+ rule(:time_hh_mi_ss_sss) { time_hh_mi_ss >> dot >> digit.repeat(3, 6) }
55
+ rule(:time_tz) { str("Z") | (match("[\+\-]") >> time_hh_mi) }
56
+ rule(:datetime_iso8601) { date_iso8601 >> str("T") >> (time_hh_mi_ss_sss | time_hh_mi_ss) >> time_tz }
57
+ rule(:datetime) { quoted datetime_iso8601.as(:datetime) }
58
+
59
+ # Operations
60
+ rule(:op_attr_binary) do
61
+ binary_attr_operators.as(:operator)
62
+ end
63
+ rule(:op_attr_unary) { (unary_attr_operators).as(:operator) }
64
+ rule(:op_logic_binary) { (str("and") | str("or")).as(:operator) }
65
+ rule(:op_logic_unary) { str("not").as(:operator) }
66
+
67
+ # Expressions
68
+ rule(:literal) { (null | boolean | decimal | integer | datetime | date | string) }
69
+ rule(:literal_paren) { lparen >> space? >> (literal | literal_paren) >> space? >> rparen }
70
+ rule(:attr_value) { literal_paren | (space >> (literal | literal_paren)) }
71
+ rule(:attr_exp) { (attribute >> space >> (op_attr_unary | (op_attr_binary >> attr_value.as(:val)))).as(:exp) }
72
+ rule(:group) do
73
+ empty_group.ignore |
74
+ (lparen >> space? >> (binary_exp | unary_exp | attr_exp) >> space? >> rparen).as(:group) |
75
+ (lparen >> space? >> group >> space? >> rparen)
76
+ end
77
+ rule(:empty_group) do
78
+ (lparen >> space? >> empty_group >> space? >> rparen) | (lparen >> space? >> rparen)
79
+ end
80
+ rule(:empty_exp) { (space | str("")).ignore }
81
+ rule(:scope_args) { literal >> space? >> (str(",") >> space? >> literal).repeat(0) }
82
+ rule(:scope) { scope_name.as(:scope) >> lparen >> space? >> scope_args.maybe.as(:args) >> space? >> rparen }
83
+ rule(:primary) { group | attr_exp | scope }
84
+
85
+ rule(:unary_exp) do
86
+ (op_logic_unary >> (space | lparen.present?) >> primary.as(:right)).as(:exp) | primary
87
+ end
88
+ rule(:binary_exp) do
89
+ (
90
+ unary_exp.as(:left) >> space >> op_logic_binary >> ((space | lparen.present?) >> exp).as(:right)
91
+ ).as(:exp) | unary_exp
92
+ end
93
+
94
+ rule(:exp) { (space? >> binary_exp >> space?) }
95
+ rule(:exp_root) { exp | empty_exp }
96
+ root(:exp_root)
97
+
98
+ def parse(expression, options = {})
99
+ super(expression, options)
100
+ rescue Parslet::ParseFailed => e
101
+ parse_cause = e.parse_failure_cause.children.last
102
+
103
+ raise_parse_error!(parse_cause)
104
+ end
105
+
106
+ private
107
+
108
+ def quoted(atom_or_seq)
109
+ (single_quote >> atom_or_seq >> single_quote) | (double_quote >> atom_or_seq >> double_quote)
110
+ end
111
+
112
+ def binary_attr_operators
113
+ @@binary_attr_ops = operators_to_atoms(Operators::FieldFilterOperator.binaries.map(&:to_s))
114
+ end
115
+
116
+ def unary_attr_operators
117
+ @@unary_attr_ops = operators_to_atoms(Operators::FieldFilterOperator.unaries.map(&:to_s))
118
+ end
119
+
120
+ def operators_to_atoms(operators)
121
+ operators.sort_by(&:length)
122
+ .reverse
123
+ .map { |tag| str(tag) }
124
+ .reduce(:|)
125
+ end
126
+
127
+ def raise_parse_error!(parse_cause)
128
+ parse_cause = parse_cause.to_s
129
+ invalid_expression = "Filter expression syntax error."
130
+
131
+ if parse_cause.start_with?("Expected ")
132
+ parse_cause = invalid_expression
133
+ else
134
+ unexpected_token = "Unexpected token"
135
+
136
+ parse_cause.sub!("Don't know what to do with", unexpected_token)
137
+ parse_cause.sub!(/(Failed to match).*.(at line 1)/, "#{unexpected_token} at")
138
+ parse_cause.sub!(/(at line 1)/, "at")
139
+ end
140
+
141
+ raise ParseError.new(parse_cause)
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,24 @@
1
+ module FilterParam
2
+ class Scope
3
+ attr_reader :name
4
+
5
+ def initialize(name, options = {})
6
+ @name = name
7
+ @rename = scope_rename(options[:rename])
8
+ end
9
+
10
+ def actual_name
11
+ rename.presence || name
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :rename
17
+
18
+ def scope_rename(rename)
19
+ return rename.call(name) if rename.is_a?(Proc)
20
+
21
+ rename
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,37 @@
1
+ module FilterParam
2
+ class Transformer < Parslet::Transform
3
+ include AST
4
+
5
+ rule(null: simple(:null)) { Literals::Null.instance }
6
+ rule(string: simple(:value)) { Literals::String.new(value) }
7
+ rule(boolean: simple(:value)) { value == "true" ? Literals::Boolean::TRUE : Literals::Boolean::FALSE }
8
+ rule(integer: simple(:value)) { Literals::Integer.new(value) }
9
+ rule(decimal: simple(:value)) { Literals::Decimal.new(value) }
10
+ rule(date: simple(:value)) { Literals::Date.new(value) }
11
+ rule(datetime: simple(:value)) { Literals::DateTime.new(value) }
12
+ rule(exp: simple(:exp)) { exp }
13
+ rule(group: simple(:exp)) { Group.new(exp) }
14
+ rule(scope: simple(:name), args: simple(:scope_arg)) do
15
+ scope_args = scope_arg.nil? ? [] : [scope_arg]
16
+
17
+ AST::Scope.new(name, scope_args)
18
+ end
19
+ rule(scope: simple(:name), args: sequence(:scope_args)) { AST::Scope.new(name, scope_args) }
20
+ rule(operator: simple(:operator), right: simple(:operand)) do
21
+ Expressions::UnaryExpression.new(operator, operand)
22
+ end
23
+ rule(attribute: simple(:attribute_name), operator: simple(:operator)) do
24
+ attribute = Attribute.new(attribute_name)
25
+
26
+ Expressions::UnaryExpression.new(operator, attribute)
27
+ end
28
+ rule(left: simple(:left), operator: simple(:operator), right: simple(:right)) do
29
+ Expressions::BinaryExpression.new(operator, left, right)
30
+ end
31
+ rule(attribute: simple(:attribute_name), operator: simple(:operator), val: simple(:literal)) do
32
+ attribute = Attribute.new(attribute_name)
33
+
34
+ Expressions::BinaryExpression.new(operator, attribute, literal)
35
+ end
36
+ end
37
+ end