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,27 @@
1
+ module Clearly
2
+ module Query
3
+
4
+ # Generic error from Clearly Query
5
+ class QueryArgumentError < ArgumentError
6
+
7
+ # @return [Hash] partial filter hash
8
+ attr_reader :filter_segment
9
+
10
+ # Create a Filter Argument Error
11
+ # @param [String] message
12
+ # @param [Hash] filter_segment
13
+ # @return [QueryArgumentError]
14
+ def initialize(message = nil, filter_segment = nil)
15
+ @message = message
16
+ @filter_segment = filter_segment
17
+ self
18
+ end
19
+
20
+ # Show a string representation of this error
21
+ # @return [String]
22
+ def to_s
23
+ @message
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,63 @@
1
+ module Clearly
2
+ module Query
3
+
4
+ # Stores a graph and provides methods to operate on the graph.
5
+ # Graph nodes are a hash, and one special key contains an array of child nodes.
6
+ class Graph
7
+
8
+ # root node
9
+ attr_reader :root_node
10
+
11
+ # name of the hash key that holds the child nodes
12
+ attr_reader :child_key
13
+
14
+ # Create a new Graph.
15
+ # @param [Array] root_node
16
+ # @param [Symbol] child_key
17
+ # @return [Clearly::Query::DepthFirstSearch]
18
+ def initialize(root_node, child_key)
19
+ @root_node = root_node
20
+ @child_key = child_key
21
+ self
22
+ end
23
+
24
+ # build an array that contains paths from the root to all leaves
25
+ # @return [Array] paths from root to leaf
26
+ def branches
27
+ @discovered_nodes = []
28
+ @paths = []
29
+ traverse_branches(@root_node, nil)
30
+ @paths
31
+ end
32
+
33
+ private
34
+
35
+ def traverse_branches(current_node, current_path)
36
+ child_nodes = current_node.include?(@child_key) ? current_node[@child_key] : []
37
+
38
+ current_node_no_children = current_node.dup.except(@child_key)
39
+
40
+ @discovered_nodes.push(current_node_no_children)
41
+
42
+
43
+ if child_nodes.size > 0
44
+ current_node[@child_key].each do |node|
45
+ child_node_no_children = node.dup.except(@child_key)
46
+
47
+ unless @discovered_nodes.include?(child_node_no_children)
48
+ node_path = current_path.nil? ? [] : current_path.dup
49
+ node_path.push(current_node_no_children)
50
+ traverse_branches(node, node_path)
51
+ end
52
+
53
+ end
54
+ else
55
+ node_path = current_path.nil? ? [] : current_path.dup
56
+ node_path.push(current_node_no_children)
57
+ @paths.push(node_path)
58
+ end
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,50 @@
1
+ module Clearly
2
+ module Query
3
+
4
+ # Utility methods for working with Arel.
5
+ class Helper
6
+ class << self
7
+
8
+ # Concatenate one or more strings
9
+ # @param [Array<String>] args strings to concatenate
10
+ # @return [Arel::Nodes::Node]
11
+ def string_concat(*args)
12
+ adapter = ActiveRecord::Base.connection.adapter_name.underscore.downcase
13
+
14
+ case adapter
15
+ when 'mysql'
16
+ Arel::Nodes::NamedFunction.new('concat', *args)
17
+ when 'sqlserver'
18
+ string_concat_infix('+', *args)
19
+ when 'postgres'
20
+ when 'sq_lite'
21
+ string_concat_infix('||', *args)
22
+ else
23
+ fail ArgumentError, "unsupported database adapter '#{adapter}'"
24
+ end
25
+ end
26
+
27
+ # Concatenate strings using an operator
28
+ # @param [Object] operator infix operator
29
+ # @param [Array<String>] args strings to concatenate
30
+ # @return [Arel::Nodes::Node]
31
+ def string_concat_infix(operator, *args)
32
+ if args.blank? || args.size < 2
33
+ fail ArgumentError, "string concatenation requires operator and two or more arguments, given '#{args.size}'"
34
+ end
35
+
36
+ result = Arel::Nodes::InfixOperation.new(operator, args[0], args[1])
37
+
38
+ if args.size > 2
39
+ args.drop(2).each do |a|
40
+ result = Arel::Nodes::InfixOperation.new(operator, result, a)
41
+ end
42
+ end
43
+
44
+ result
45
+ end
46
+
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,296 @@
1
+ module Clearly
2
+ module Query
3
+
4
+ # Provides common validations for composing queries.
5
+ module Validate
6
+
7
+ # Validate query, table, and column values.
8
+ # @param [Arel::Query] query
9
+ # @param [Arel::Table] table
10
+ # @param [Symbol] column_name
11
+ # @param [Array<Symbol>] allowed
12
+ # @return [void]
13
+ def validate_query_table_column(query, table, column_name, allowed)
14
+ validate_query(query)
15
+ validate_table(table)
16
+ validate_name(column_name, allowed)
17
+ end
18
+
19
+ # Validate table and column values.
20
+ # @param [Arel::Table] table
21
+ # @param [Symbol] column_name
22
+ # @param [Array<Symbol>] allowed
23
+ # @return [void]
24
+ def validate_table_column(table, column_name, allowed)
25
+ validate_table(table)
26
+ validate_name(column_name, allowed)
27
+ end
28
+
29
+ # Validate model association.
30
+ # @param [ActiveRecord::Base] model
31
+ # @param [Array<ActiveRecord::Base>] models_allowed
32
+ # @return [void]
33
+ def validate_association(model, models_allowed)
34
+ validate_model(model)
35
+ validate_not_blank(models_allowed)
36
+ validate_array(models_allowed)
37
+
38
+ fail Clearly::Query::QueryArgumentError, "models allowed must be an Array, got '#{models_allowed}'" unless models_allowed.is_a?(Array)
39
+ fail Clearly::Query::QueryArgumentError, "model must be in '#{models_allowed}', got '#{model}'" unless models_allowed.include?(model)
40
+ end
41
+
42
+ # Validate query and hash values.
43
+ # @param [ActiveRecord::Relation] query
44
+ # @param [Hash] hash
45
+ # @return [void]
46
+ def validate_query_hash(query, hash)
47
+ validate_query(query)
48
+ validate_hash(hash)
49
+ end
50
+
51
+ # Validate table value.
52
+ # @param [Arel::Table] table
53
+ # @raise [FilterArgumentError] if table is not an Arel::Table
54
+ # @return [void]
55
+ def validate_table(table)
56
+ fail Clearly::Query::QueryArgumentError, "table must be Arel::Table, got '#{table.class}'" unless table.is_a?(Arel::Table)
57
+ end
58
+
59
+ # Validate table value.
60
+ # @param [ActiveRecord::Relation] query
61
+ # @raise [FilterArgumentError] if query is not an Arel::Query
62
+ # @return [void]
63
+ def validate_query(query)
64
+ fail Clearly::Query::QueryArgumentError, "query must be ActiveRecord::Relation, got '#{query.class}'" unless query.is_a?(ActiveRecord::Relation)
65
+ end
66
+
67
+ # Validate condition value.
68
+ # @param [Arel::Nodes::Node] condition
69
+ # @raise [FilterArgumentError] if condition is not an Arel::Nodes::Node
70
+ # @return [void]
71
+ def validate_condition(condition)
72
+ if !condition.is_a?(Arel::Nodes::Node) && !condition.is_a?(String)
73
+ fail Clearly::Query::QueryArgumentError, "condition must be Arel::Nodes::Node or String, got '#{condition}'"
74
+ end
75
+ end
76
+
77
+ # Validate value is a node or attribute
78
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] value
79
+ # @return [void]
80
+ def validate_node_or_attribute(value)
81
+ check = value.is_a?(Arel::Nodes::Node) || value.is_a?(String) || value.is_a?(Arel::Attributes::Attribute) || value.is_a?(Symbol)
82
+ fail Clearly::Query::QueryArgumentError, "value must be Arel::Nodes::Node or String or Symbol or Arel::Attributes::Attribute, got '#{value}'" unless check
83
+ end
84
+
85
+ # Validate name value.
86
+ # @param [Symbol] name
87
+ # @param [Array<Symbol>] allowed
88
+ # @raise [FilterArgumentError] if name is not a symbol in allowed
89
+ # @return [void]
90
+ def validate_name(name, allowed)
91
+ validate_not_blank(name)
92
+ fail Clearly::Query::QueryArgumentError, "name must be a symbol, got '#{name}'" unless name.is_a?(Symbol)
93
+ fail Clearly::Query::QueryArgumentError, "allowed must be an Array, got '#{allowed}'" unless allowed.is_a?(Array)
94
+ fail Clearly::Query::QueryArgumentError, "name must be in '#{allowed}', got '#{name}'" unless allowed.include?(name)
95
+ end
96
+
97
+ # Validate model value.
98
+ # @param [ActiveRecord::Base] model
99
+ # @raise [FilterArgumentError] if model is not an ActiveRecord::Base
100
+ # @return [void]
101
+ def validate_model(model)
102
+ validate_not_blank(model)
103
+ fail Clearly::Query::QueryArgumentError, "model must be an ActiveRecord::Base, got '#{model.base_class}'" unless model < ActiveRecord::Base
104
+ end
105
+
106
+ # Validate an array.
107
+ # @param [Array, Arel::SelectManager] value
108
+ # @raise [FilterArgumentError] if value is not a valid Array.
109
+ # @return [void]
110
+ def validate_array(value)
111
+ fail Clearly::Query::QueryArgumentError, "value must be an Array or Arel::SelectManager, got '#{value.class}'" unless value.is_a?(Array) || value.is_a?(Arel::SelectManager)
112
+ end
113
+
114
+ # Validate array items. Do not validate if value is not an Array.
115
+ # @param [Array] value
116
+ # @raise [FilterArgumentError] if Array contents are not valid.
117
+ # @return [void]
118
+ def validate_array_items(value)
119
+ # must be a collection of items
120
+ if !value.respond_to?(:each) || !value.respond_to?(:all?) || !value.respond_to?(:any?) || !value.respond_to?(:count)
121
+ fail Clearly::Query::QueryArgumentError, "must be a collection of items, got '#{value.class}'"
122
+ end
123
+
124
+ # if there are no items, let it through
125
+ if value.count > 0
126
+ # all items must be the same type (or a subclass). Assume the first item is the correct type.
127
+ type_compare_item = value[0].class
128
+ type_compare = value.all? do |item|
129
+ is_same_class = item.is_a?(type_compare_item)
130
+ item_class = item.class
131
+ is_same_class ? true : (item_class <= Arel::Nodes::Node && type_compare_item <= Arel::Nodes::Node)
132
+ end
133
+ fail Clearly::Query::QueryArgumentError, "array values must be a single consistent type, got '#{value.map { |v| v.class.name }.join(', ')}'" unless type_compare
134
+
135
+ # restrict length of strings
136
+ if type_compare_item.is_a?(String)
137
+ max_string_length = 120
138
+ string_length = value.all? { |item| item.size <= max_string_length }
139
+ fail Clearly::Query::QueryArgumentError, "array values that are strings must be '#{max_string_length}' characters or less" unless string_length
140
+ end
141
+
142
+ # array contents cannot be Arrays or Hashes
143
+ array_check = value.any? { |item| item.is_a?(Array) }
144
+ fail Clearly::Query::QueryArgumentError, 'array values cannot be arrays' if array_check
145
+
146
+ hash_check = value.any? { |item| item.is_a?(Hash) }
147
+ fail Clearly::Query::QueryArgumentError, 'array values cannot be hashes' if hash_check
148
+
149
+ end
150
+ end
151
+
152
+ # Validate a hash.
153
+ # @param [Array] value
154
+ # @raise [FilterArgumentError] if value is not a valid Hash.
155
+ # @return [void]
156
+ def validate_hash(value)
157
+ validate_not_blank(value)
158
+ fail Clearly::Query::QueryArgumentError, "value must be a Hash, got '#{value}'" unless value.is_a?(Hash)
159
+ end
160
+
161
+ # Validate a symbol.
162
+ # @param [Symbol] value
163
+ # @raise [FilterArgumentError] if value is not a Symbol.
164
+ # @return [void]
165
+ def validate_symbol(value)
166
+ validate_not_blank(value)
167
+ fail Clearly::Query::QueryArgumentError, "value must be a Symbol, got '#{value}'" unless value.is_a?(Symbol)
168
+ end
169
+
170
+ # Validate value is not blank
171
+ # @param [Object] value
172
+ # @return [void]
173
+ def validate_not_blank(value)
174
+ fail Clearly::Query::QueryArgumentError, "value must not be empty, got '#{value}'" if value.blank?
175
+ end
176
+
177
+ # Validate value is a boolean
178
+ # @param [Boolean] value
179
+ # @return [void]
180
+ def validate_boolean(value)
181
+ fail Clearly::Query::QueryArgumentError, "value must be a boolean, got '#{value}'" if !value.is_a?(TrueClass) && !value.is_a?(FalseClass)
182
+ end
183
+
184
+ # Escape wildcards in like value..
185
+ # @param [String] value
186
+ # @return [String] sanitized value
187
+ def sanitize_like_value(value)
188
+ value.gsub(/[\\_%\|]/) { |x| "\\#{x}" }
189
+ end
190
+
191
+ # Escape meta-characters in SIMILAR TO value.
192
+ # see http://www.postgresql.org/docs/9.3/static/functions-matching.html
193
+ # @param [String] value
194
+ # @return [String] sanitized value
195
+ def sanitize_similar_to_value(value)
196
+ value.gsub(/[\\_%\|\*\+\?\{\}\(\)\[\]]/) { |x| "\\#{x}" }
197
+ end
198
+
199
+ # Create LIKE syntax.
200
+ # @param [String] value
201
+ # @param [Hash] options
202
+ # @return [String]
203
+ def like_syntax(value, options = {start: false, end: false})
204
+ "#{options[:start] ? '%' : ''}#{sanitize_like_value(value)}#{options[:end] ? '%' : ''}"
205
+ end
206
+
207
+ # validate an integer
208
+ # @param [Object] value
209
+ # @param [Integer] min
210
+ # @param [Integer] max
211
+ # @return [void]
212
+ def validate_integer(value, min = nil, max = nil)
213
+ validate_not_blank(value)
214
+ fail Clearly::Query::QueryArgumentError, "value must be an integer, got '#{value}'" if value != value.to_i
215
+
216
+ value_i = value.to_i
217
+
218
+ fail Clearly::Query::QueryArgumentError, "value must be '#{min}' or greater, got '#{value_i}'" if !min.blank? && value_i < min
219
+ fail Clearly::Query::QueryArgumentError, "value must be '#{max}' or less, got '#{value_i}'" if !max.blank? && value_i > max
220
+ end
221
+
222
+ # Check that value is a float.
223
+ # @param [Object] value
224
+ # @raise [FilterArgumentError] if value is not a float
225
+ # @return [void]
226
+ def validate_float(value)
227
+ validate_not_blank(value)
228
+
229
+ filtered = value.to_s.tr('^0-9.', '')
230
+ fail Clearly::Query::QueryArgumentError, "value must be a float, got '#{filtered}'" if filtered != value
231
+ fail Clearly::Query::QueryArgumentError, "value must be a float after conversion, got '#{filtered}'" if filtered != value.to_f
232
+
233
+ value_f = filtered.to_f
234
+ fail Clearly::Query::QueryArgumentError, "value must be greater than 0, got '#{value_f}'" if value_f <= 0
235
+
236
+ end
237
+
238
+ # Validate definition instance
239
+ # @param [Clearly::Query::Definition] value
240
+ # @return [void]
241
+ def validate_definition_instance(value)
242
+ validate_not_blank(value)
243
+ fail Clearly::Query::QueryArgumentError, "value must be a model definition, got '#{value.class}'" unless value.is_a?(Clearly::Query::Definition)
244
+ end
245
+
246
+ # Validate definition specification
247
+ # @param [Hash] value
248
+ # @return [void]
249
+ def validate_definition(value)
250
+ validate_hash(value)
251
+
252
+ # fields
253
+ validate_hash(value[:fields])
254
+
255
+ validate_not_blank(value[:fields][:valid])
256
+ validate_array(value[:fields][:valid])
257
+ validate_array_items(value[:fields][:valid])
258
+
259
+ validate_not_blank(value[:fields][:text])
260
+ validate_array(value[:fields][:text])
261
+ validate_array_items(value[:fields][:text])
262
+
263
+ validate_not_blank(value[:fields][:mappings])
264
+ validate_array(value[:fields][:mappings])
265
+
266
+ value[:fields][:mappings].each do |mapping|
267
+ validate_hash(mapping)
268
+ validate_symbol(mapping[:name])
269
+ validate_not_blank(mapping[:value])
270
+ end
271
+
272
+ # associations
273
+ validate_spec_association(value[:associations])
274
+
275
+ # defaults
276
+ validate_hash(value[:defaults])
277
+ end
278
+
279
+ # Validate association specification
280
+ # @param [Array] value
281
+ # @return [void]
282
+ def validate_spec_association(value)
283
+ validate_array(value)
284
+
285
+ value.each do |association|
286
+ validate_hash(association)
287
+ validate_not_blank(association[:join])
288
+ validate_not_blank(association[:on])
289
+ validate_boolean(association[:available])
290
+ validate_spec_association(association[:associations]) if association.include?(:associations)
291
+ end
292
+ end
293
+
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,8 @@
1
+ # Clearly Query namespace
2
+ module Clearly
3
+ # Clearly Query namespace
4
+ module Query
5
+ # Gem version
6
+ VERSION = '0.3.1.pre'
7
+ end
8
+ end