clearly-query 0.3.1.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +8 -0
  3. data/.gitignore +42 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +10 -0
  6. data/CHANGELOG.md +43 -0
  7. data/Gemfile +7 -0
  8. data/Guardfile +19 -0
  9. data/LICENSE +22 -0
  10. data/README.md +102 -0
  11. data/Rakefile +7 -0
  12. data/SPEC.md +101 -0
  13. data/bin/guard +16 -0
  14. data/bin/rake +16 -0
  15. data/bin/rspec +16 -0
  16. data/bin/yard +16 -0
  17. data/clearly-query.gemspec +33 -0
  18. data/lib/clearly/query.rb +22 -0
  19. data/lib/clearly/query/cleaner.rb +63 -0
  20. data/lib/clearly/query/compose/comparison.rb +102 -0
  21. data/lib/clearly/query/compose/conditions.rb +215 -0
  22. data/lib/clearly/query/compose/core.rb +75 -0
  23. data/lib/clearly/query/compose/custom.rb +268 -0
  24. data/lib/clearly/query/compose/range.rb +114 -0
  25. data/lib/clearly/query/compose/special.rb +24 -0
  26. data/lib/clearly/query/compose/subset.rb +115 -0
  27. data/lib/clearly/query/composer.rb +269 -0
  28. data/lib/clearly/query/definition.rb +165 -0
  29. data/lib/clearly/query/errors.rb +27 -0
  30. data/lib/clearly/query/graph.rb +63 -0
  31. data/lib/clearly/query/helper.rb +50 -0
  32. data/lib/clearly/query/validate.rb +296 -0
  33. data/lib/clearly/query/version.rb +8 -0
  34. data/spec/lib/clearly/query/cleaner_spec.rb +42 -0
  35. data/spec/lib/clearly/query/compose/custom_spec.rb +77 -0
  36. data/spec/lib/clearly/query/composer_query_spec.rb +50 -0
  37. data/spec/lib/clearly/query/composer_spec.rb +422 -0
  38. data/spec/lib/clearly/query/definition_spec.rb +23 -0
  39. data/spec/lib/clearly/query/graph_spec.rb +81 -0
  40. data/spec/lib/clearly/query/helper_spec.rb +17 -0
  41. data/spec/lib/clearly/query/version_spec.rb +7 -0
  42. data/spec/spec_helper.rb +89 -0
  43. data/spec/support/db/migrate/001_db_create.rb +62 -0
  44. data/spec/support/models/customer.rb +63 -0
  45. data/spec/support/models/order.rb +66 -0
  46. data/spec/support/models/part.rb +63 -0
  47. data/spec/support/models/product.rb +67 -0
  48. data/spec/support/shared_setup.rb +13 -0
  49. data/tmp/.gitkeep +0 -0
  50. metadata +263 -0
@@ -0,0 +1,24 @@
1
+ module Clearly
2
+ module Query
3
+ module Compose
4
+
5
+ # Methods for composing queries containing spacial comparisons.
6
+ module Special
7
+ include Clearly::Query::Validate
8
+
9
+ private
10
+
11
+ # Create null comparison node.
12
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
13
+ # @param [Boolean] value
14
+ # @return [Arel::Nodes::Node] condition
15
+ def compose_null_node(node, value)
16
+ validate_node_or_attribute(node)
17
+ validate_boolean(value)
18
+ value ? node.eq(nil) : node.not_eq(nil)
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,115 @@
1
+ module Clearly
2
+ module Query
3
+ module Compose
4
+
5
+ # Methods for composing subset queries.
6
+ module Subset
7
+ include Clearly::Query::Validate
8
+
9
+ private
10
+
11
+ # Create contains condition.
12
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
13
+ # @param [Object] value
14
+ # @return [Arel::Nodes::Node] condition
15
+ def compose_contains_node(node, value)
16
+ validate_node_or_attribute(node)
17
+ node.matches(like_syntax(value, {start: true, end: true}))
18
+ end
19
+
20
+ # Create not contains condition.
21
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
22
+ # @param [Object] value
23
+ # @return [Arel::Nodes::Node] condition
24
+ def compose_not_contains_node(node, value)
25
+ validate_node_or_attribute(node)
26
+ node.does_not_match(like_syntax(value, {start: true, end: true}))
27
+ end
28
+
29
+ # Create starts_with condition.
30
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
31
+ # @param [Object] value
32
+ # @return [Arel::Nodes::Node] condition
33
+ def compose_starts_with_node(node, value)
34
+ validate_node_or_attribute(node)
35
+ node.matches(like_syntax(value, {start: false, end: true}))
36
+ end
37
+
38
+ # Create not starts_with condition.
39
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
40
+ # @param [Object] value
41
+ # @return [Arel::Nodes::Node] condition
42
+ def compose_not_starts_with_node(node, value)
43
+ validate_node_or_attribute(node)
44
+ node.does_not_match(like_syntax(value, {start: false, end: true}))
45
+ end
46
+
47
+ # Create ends_with condition.
48
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
49
+ # @param [Object] value
50
+ # @return [Arel::Nodes::Node] condition
51
+ def compose_ends_with_node(node, value)
52
+ validate_node_or_attribute(node)
53
+ node.matches(like_syntax(value, {start: true, end: false}))
54
+ end
55
+
56
+ # Create not ends_with condition.
57
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
58
+ # @param [Object] value
59
+ # @return [Arel::Nodes::Node] condition
60
+ def compose_not_ends_with_node(node, value)
61
+ validate_node_or_attribute(node)
62
+ node.does_not_match(like_syntax(value, {start: true, end: false}))
63
+ end
64
+
65
+ # Create IN condition.
66
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
67
+ # @param [Array] values
68
+ # @return [Arel::Nodes::Node] condition
69
+ def compose_in_node(node, values)
70
+ validate_node_or_attribute(node)
71
+ values = [values].flatten
72
+ validate_not_blank(values)
73
+ validate_array(values)
74
+ validate_array_items(values)
75
+ node.in(values)
76
+ end
77
+
78
+ # Create NOT IN condition.
79
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
80
+ # @param [Array] values
81
+ # @return [Arel::Nodes::Node] condition
82
+ def compose_not_in_node(node, values)
83
+ validate_node_or_attribute(node)
84
+ values = [values].flatten
85
+ validate_not_blank(values)
86
+ validate_array(values)
87
+ validate_array_items(values)
88
+ node.not_in(values)
89
+ end
90
+
91
+ # Create regular expression condition.
92
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
93
+ # @param [Object] value
94
+ # @return [Arel::Nodes::Node] condition
95
+ def compose_regex_node(node, value)
96
+ validate_node_or_attribute(node)
97
+ sanitized_value = sanitize_similar_to_value(value)
98
+ Arel::Nodes::Regexp.new(node, Arel::Nodes.build_quoted(sanitized_value))
99
+ end
100
+
101
+ # Create negated regular expression condition.
102
+ # Not available just now, maybe in Arel 6?
103
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
104
+ # @param [Object] value
105
+ # @return [Arel::Nodes::Node] condition
106
+ def compose_not_regex_node(node, value)
107
+ validate_node_or_attribute(node)
108
+ sanitized_value = sanitize_similar_to_value(value)
109
+ Arel::Nodes::NotRegexp.new(node, Arel::Nodes.build_quoted(sanitized_value))
110
+ end
111
+
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,269 @@
1
+ module Clearly
2
+ module Query
3
+
4
+ # Class that composes a query from a filter hash.
5
+ class Composer
6
+ include Clearly::Query::Compose::Conditions
7
+ include Clearly::Query::Validate
8
+
9
+ # @return [Array<Clearly::Query::Definition>] available definitions
10
+ attr_reader :definitions
11
+
12
+ # Create an instance of Composer using a set of model query spec definitions.
13
+ # @param [Array<Clearly::Query::Definition>] definitions
14
+ # @return [Clearly::Query::Composer]
15
+ def initialize(definitions)
16
+ validate_not_blank(definitions)
17
+ validate_array(definitions)
18
+ validate_definition_instance(definitions[0])
19
+ validate_array_items(definitions)
20
+ @definitions = definitions
21
+ @table_names = definitions.map { |d| d.table.name }
22
+ self
23
+ end
24
+
25
+ # Create an instance of Composer from all ActiveRecord models.
26
+ # @return [Clearly::Query::Composer]
27
+ def self.from_active_record
28
+ models = ActiveRecord::Base
29
+ .descendants
30
+ .reject { |d| d.name == 'ActiveRecord::SchemaMigration' }
31
+ .sort { |a, b| a.name <=> b.name }
32
+ .uniq { |d| d.arel_table.name }
33
+
34
+ definitions = models.map do |d|
35
+ if d.name.include?('HABTM_')
36
+ Clearly::Query::Definition.new({table: d.arel_table})
37
+ else
38
+ Clearly::Query::Definition.new({model: d, hash: d.clearly_query_def})
39
+ end
40
+ end
41
+
42
+ Composer.new(definitions)
43
+ end
44
+
45
+ # Composes a query from a parsed filter hash.
46
+ # @param [ActiveRecord::Base] model
47
+ # @param [Hash] hash
48
+ # @return [Arel::Nodes::Node, Array<Arel::Nodes::Node>]
49
+ def query(model, hash)
50
+ definition = select_definition_from_model(model)
51
+ # default combiner is :and
52
+ parse_query(definition, :and, hash)
53
+ end
54
+
55
+ private
56
+
57
+ # figure out which model spec to use as the base from the table
58
+ # select from available definitions
59
+ # @param [Arel::Table] table
60
+ # @return [Clearly::Query::Definition]
61
+ def select_definition_from_table(table)
62
+ validate_table(table)
63
+ matches = @definitions.select { |definition| definition.table.name == table.name }
64
+ if matches.size != 1
65
+ fail Clearly::Query::QueryArgumentError, "exactly one definition must match, found '#{matches.size}'"
66
+ end
67
+
68
+ matches.first
69
+ end
70
+
71
+ # figure out which model spec to use as the base from the model
72
+ # select rom available definitions
73
+ # @param [ActiveRecord::Base] model
74
+ # @return [Clearly::Query::Definition]
75
+ def select_definition_from_model(model)
76
+ validate_model(model)
77
+ select_definition_from_table(model.arel_table)
78
+ end
79
+
80
+ # Parse a filter hash.
81
+ # @param [Clearly::Query::Definition] definition
82
+ # @param [Symbol] query_key
83
+ # @param [Hash] query_value
84
+ # @return [Array<Arel::Nodes::Node>]
85
+ def parse_query(definition, query_key, query_value)
86
+ if query_value.blank? || query_value.size < 1
87
+ msg = "filter hash must have at least 1 entry, got '#{query_value.size}'"
88
+ fail Clearly::Query::QueryArgumentError.new(msg, {hash: query_value})
89
+ end
90
+
91
+ logical_operators = Clearly::Query::Compose::Conditions::OPERATORS_LOGICAL
92
+
93
+ mapped_fields = definition.field_mappings.keys
94
+ standard_fields = definition.all_fields - mapped_fields
95
+
96
+ conditions = []
97
+
98
+ if logical_operators.include?(query_key)
99
+ # first deal with logical operators
100
+ condition = parse_logical_operator(definition, query_key, query_value)
101
+ conditions.push(condition)
102
+
103
+ elsif standard_fields.include?(query_key)
104
+ # then cater for standard fields
105
+ field_conditions = parse_standard_field(definition, query_key, query_value)
106
+ conditions.push(*field_conditions)
107
+
108
+ elsif mapped_fields.include?(query_key)
109
+ # then deal with mapped fields
110
+ field_conditions = parse_mapped_field(definition, query_key, query_value)
111
+ conditions.push(*field_conditions)
112
+
113
+ elsif @table_names.any? { |tn| query_key.to_s.downcase.start_with?(tn) }
114
+ # finally deal with fields from other tables
115
+ field_conditions = parse_custom(definition, query_key, query_value)
116
+ conditions.push(field_conditions)
117
+ else
118
+ fail Clearly::Query::QueryArgumentError.new("unrecognised operator or field '#{query_key}'")
119
+ end
120
+
121
+ conditions
122
+
123
+ end
124
+
125
+ # Parse a logical operator and it's value.
126
+ # @param [Clearly::Query::Definition] definition
127
+ # @param [Symbol] logical_operator
128
+ # @param [Hash] value
129
+ # @return [Arel::Nodes::Node]
130
+ def parse_logical_operator(definition, logical_operator, value)
131
+ validate_definition_instance(definition)
132
+ validate_symbol(logical_operator)
133
+ validate_hash(value)
134
+ conditions = value.map { |key, value| parse_query(definition, key, value) }
135
+ condition_combine(logical_operator, *conditions)
136
+ end
137
+
138
+ # Parse a standard field and it's conditions.
139
+ # @param [Clearly::Query::Definition] definition
140
+ # @param [Symbol] field
141
+ # @param [Hash] value
142
+ # @return [Array<Arel::Nodes::Node>]
143
+ def parse_standard_field(definition, field, value)
144
+ validate_definition_instance(definition)
145
+ validate_symbol(field)
146
+ validate_hash(value)
147
+ value.map do |operator, operation_value|
148
+ condition_components(operator, definition.table, field, definition.all_fields, operation_value)
149
+ end
150
+ end
151
+
152
+ # Parse a mapped field and it's conditions.
153
+ # @param [Clearly::Query::Definition] definition
154
+ # @param [Symbol] field
155
+ # @param [Hash] value
156
+ # @return [Array<Arel::Nodes::Node>]
157
+ def parse_custom(definition, field, value)
158
+ validate_definition_instance(definition)
159
+ validate_symbol(field)
160
+ fail Clearly::Query::QueryArgumentError.new('field name must contain a dot (.)') unless field.to_s.include?('.')
161
+
162
+ validate_hash(value)
163
+
164
+ # extract table and field
165
+ dot_index = field.to_s.index('.')
166
+
167
+ other_table = field[0, dot_index].to_sym
168
+ other_model = other_table.to_s.classify.constantize
169
+ other_field = field[(dot_index + 1)..field.length].to_sym
170
+
171
+ table_names = definition.associations_flat.map { |a| a[:join].table_name.to_sym }
172
+ validate_name(other_table, table_names)
173
+
174
+ models = definition.associations_flat.map { |a| a[:join] }
175
+ validate_association(other_model, models)
176
+
177
+ other_definition = select_definition_from_model(other_model)
178
+
179
+ conditions = parse_standard_field(other_definition, other_field, value)
180
+ subquery(definition, other_definition, conditions)
181
+ end
182
+
183
+ # Parse a mapped field
184
+ # @param [Clearly::Query::Definition] definition
185
+ # @param [Symbol] field mapped field
186
+ # @param [Hash] value
187
+ # @return [Array<Arel::Nodes::Node>]
188
+ def parse_mapped_field(definition, field, value)
189
+ validate_definition_instance(definition)
190
+ mapping = definition.get_field_mapping(field)
191
+ validate_node_or_attribute(mapping)
192
+ validate_hash(value)
193
+ value.map do |operator, operation_value|
194
+ condition_node(operator, mapping, operation_value)
195
+ end
196
+ end
197
+
198
+ # Build a subquery restricting definition to conditions on other_definition.
199
+ # @param [Clearly::Query::Definition] definition
200
+ # @param [Clearly::Query::Definition] other_definition
201
+ # @param [Array<Arel::Nodes::Node>] conditions
202
+ # @return [Array<Arel::Nodes::Node>]
203
+ def subquery(definition, other_definition, conditions)
204
+ validate_definition_instance(definition)
205
+ validate_definition_instance(other_definition)
206
+ [conditions].flatten.each { |c| validate_node_or_attribute(c) }
207
+
208
+ current_model = definition.model
209
+ current_table = definition.table
210
+ current_joins = definition.joins
211
+
212
+ other_table = other_definition.table
213
+ other_model = other_definition.model
214
+ other_joins = other_definition.joins
215
+
216
+ # build an exist subquery to apply conditions that
217
+ # refer to another table
218
+
219
+ subquery = other_definition.table
220
+
221
+ # add conditions to subquery
222
+ [conditions].flatten.each do |c|
223
+ subquery = subquery.where(c)
224
+ end
225
+
226
+ # add joins that provide other table access to current table
227
+
228
+
229
+ which_joins = current_joins
230
+ join_paths_index = nil
231
+ join_path_current_index = nil
232
+ join_path_other_index = nil
233
+ which_joins.each_with_index do |item, index|
234
+ join_path_current_index = item.find_index { |j| j[:join] == current_model }
235
+ join_path_other_index = item.find_index { |j| j[:join] == other_model }
236
+ if !join_path_current_index.nil? && !join_path_other_index.nil?
237
+ join_paths_index = index
238
+ break
239
+ end
240
+ end
241
+
242
+ first_index = [join_path_current_index, join_path_other_index].min
243
+ last_index = [join_path_current_index, join_path_other_index].max
244
+ relevant_joins = which_joins[join_paths_index][first_index..last_index]
245
+
246
+
247
+ relevant_joins.each do |j|
248
+ join_table = j[:join]
249
+ join_condition = j[:on]
250
+
251
+ # assume this is an arel_table if it doesn't respond to .arel_table
252
+ arel_table = join_table.respond_to?(:arel_table) ? join_table.arel_table : join_table
253
+
254
+ if arel_table.name == other_table.name && !join_condition.nil?
255
+ # add join as condition if this is the main table in the subquery
256
+ subquery = subquery.where(join_condition)
257
+ elsif arel_table.name != other_table.name && !join_condition.nil?
258
+ # add full join if this is not the main table in the subquery
259
+ subquery = subquery.join(arel_table).on(join_condition)
260
+ end
261
+
262
+ end
263
+
264
+ subquery.project(1).exists
265
+ end
266
+
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,165 @@
1
+ module Clearly
2
+ module Query
3
+
4
+ # Validates and represents a model query specification definition.
5
+ class Definition
6
+ include Clearly::Query::Compose::Comparison
7
+ include Clearly::Query::Compose::Core
8
+ include Clearly::Query::Compose::Range
9
+ include Clearly::Query::Compose::Subset
10
+ include Clearly::Query::Compose::Special
11
+ include Clearly::Query::Validate
12
+
13
+ # @return [ActiveRecord::Base] active record model for this definition
14
+ attr_reader :model
15
+
16
+ # @return [Arel::Table] arel table for this definition
17
+ attr_reader :table
18
+
19
+ # @return [Array<Symbol>] available model fields
20
+ attr_reader :all_fields
21
+
22
+ # @return [Array<Symbol>] available text model fields
23
+ attr_reader :text_fields
24
+
25
+ # @return [Array<Hash>] mapped model fields
26
+ attr_reader :field_mappings
27
+
28
+ # @return [Array<Hash>] model associations hierarchy
29
+ attr_reader :associations
30
+
31
+ # @return [Array<Hash>] model associations flat array
32
+ attr_reader :associations_flat
33
+
34
+ # @return [Array<Array<Hash>>] associations organised to calculate joins
35
+ attr_reader :joins
36
+
37
+ # @return [Hash] defaults
38
+ attr_reader :defaults
39
+
40
+ # Create a Definition
41
+ # @param [Hash] opts the options to create a message with.
42
+ # @option opts [ActiveRecord::Base] :model (nil) the ActiveRecord model
43
+ # @option opts [Hash] :hash (nil) the model definition hash
44
+ # @option opts [Arel::Table] :table (nil) the arel table
45
+ # @return [Clearly::Query::Definition]
46
+ def initialize(opts)
47
+ opts = {model: nil, hash: nil, table: nil}.merge(opts)
48
+
49
+ # two ways to go: model and hash, or table and joins
50
+ result = nil
51
+ result = create_from_model(opts[:model], opts[:hash]) unless opts[:model].nil?
52
+ result = create_from_table(opts[:table]) if result.nil? && !opts[:table].nil?
53
+
54
+ fail Clearly::Query::QueryArgumentError.new('could not build definition from options') if result.nil?
55
+ result
56
+ end
57
+
58
+ # Build custom field from model mappings
59
+ # @param [Symbol] column_name
60
+ # @return [Arel::Nodes::Node, Arel::Attributes::Attribute, String]
61
+ def get_field_mapping(column_name)
62
+ value = @field_mappings[column_name]
63
+ if @field_mappings.keys.include?(column_name) && !value.blank?
64
+ value
65
+ else
66
+ nil
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ # Create a Definition from an ActiveRecord model.
73
+ # @param [ActiveRecord::Base] model the ActiveRecord model
74
+ # @param [Hash] hash the model definition hash
75
+ # @return [Clearly::Query::Definition]
76
+ def create_from_model(model, hash)
77
+ validate_model(model)
78
+ validate_definition(hash)
79
+
80
+ @model = model
81
+ @table = relation_table(model)
82
+
83
+ @all_fields = hash[:fields][:valid]
84
+ @text_fields = hash[:fields][:text]
85
+
86
+ mappings = {}
87
+ hash[:fields][:mappings].each { |m| mappings[m[:name]] = m[:value] }
88
+ @field_mappings = mappings
89
+
90
+ @associations = hash[:associations]
91
+ @associations_flat = build_associations_flat(@associations)
92
+
93
+ if @associations.size > 0
94
+ node = {join: model, on: nil, associations: hash[:associations]}
95
+ graph = Clearly::Query::Graph.new(node, :associations)
96
+ @joins = graph.branches
97
+ else
98
+ @joins = []
99
+ end
100
+
101
+ @defaults = hash[:defaults]
102
+
103
+ self
104
+ end
105
+
106
+ # Create a Definition for a has and belongs to many table.
107
+ # @param [Arel::Table] table the arel table
108
+ # @return [Clearly::Query::Definition]
109
+ def create_from_table(table)
110
+ validate_table(table)
111
+
112
+ @model = nil
113
+ @table = table
114
+ @all_fields = []
115
+ @text_fields = []
116
+ @field_mappings = []
117
+ @associations = []
118
+ @associations_flat = []
119
+ @joins = []
120
+ @defaults = {}
121
+
122
+ table_name = table.name
123
+ associated_table_names = table_name.split('_')
124
+
125
+ # assumes associated tables primary key is 'id'
126
+ # assumes associated table names are the plural version of HABTM _id columns
127
+ associated_table_names.each do |t|
128
+ arel_table = Arel::Table.new(t.to_sym)
129
+ id_column = "#{t.singularize}_id"
130
+ join = {join: arel_table, on: arel_table[:id].eq(table[id_column]), available: true}
131
+
132
+ @all_fields.push(id_column)
133
+ @associations.push(join)
134
+ @associations_flat.push(join)
135
+ @joins.push([join])
136
+ end
137
+
138
+ self
139
+ end
140
+
141
+ # Create a flat array of joins.
142
+ # @param [Array<Hash>] associations
143
+ # @return [Array<Hash>] associations
144
+ def build_associations_flat(associations)
145
+ joins = []
146
+
147
+ if associations.is_a?(Array)
148
+ more_associations = associations.map { |i| build_associations_flat(i) }
149
+ joins.push(*more_associations.flatten.compact) if more_associations.size > 0
150
+
151
+ elsif associations.is_a?(Hash)
152
+ joins.push(associations.except(:associations))
153
+
154
+ if associations[:associations] && associations[:associations].size > 0
155
+ more_associations = build_associations_flat(associations[:associations])
156
+ joins.push(*more_associations.compact) if more_associations.size > 0
157
+ end
158
+ end
159
+
160
+ joins.uniq
161
+ end
162
+
163
+ end
164
+ end
165
+ end