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,75 @@
1
+ module Clearly
2
+ module Query
3
+ module Compose
4
+
5
+ # Provides 'and', and 'or' for composing queries.
6
+ module Core
7
+ include Clearly::Query::Validate
8
+
9
+ private
10
+
11
+ # Get the ActiveRecord::Relation that represents zero records.
12
+ # @param [ActiveRecord::Base] model
13
+ # @return [ActiveRecord::Relation] query that will get zero records
14
+ def relation_none(model)
15
+ validate_model(model)
16
+ model.none
17
+ end
18
+
19
+ # Get the ActiveRecord::Relation that represents all records.
20
+ # @param [ActiveRecord::Base] model
21
+ # @return [ActiveRecord::Relation] query that will get all records
22
+ def relation_all(model)
23
+ validate_model(model)
24
+ model.all
25
+ end
26
+
27
+ # Get the Arel::Table for this model.
28
+ # @param [ActiveRecord::Base] model
29
+ # @return [Arel::Table] arel table
30
+ def relation_table(model)
31
+ validate_model(model)
32
+ model.arel_table
33
+ end
34
+
35
+ # Join conditions using or.
36
+ # @param [Arel::Nodes::Node] first_condition
37
+ # @param [Arel::Nodes::Node] second_condition
38
+ # @return [Arel::Nodes::Node] condition
39
+ def compose_or(first_condition, second_condition)
40
+ validate_condition(first_condition)
41
+ validate_condition(second_condition)
42
+ first_condition.or(second_condition)
43
+ end
44
+
45
+ # Join conditions using and.
46
+ # @param [Arel::Nodes::Node] first_condition
47
+ # @param [Arel::Nodes::Node] second_condition
48
+ # @param [Array<Arel::Nodes::Node>] conditions
49
+ # @return [Arel::Nodes::Node] condition
50
+ def compose_and(first_condition, second_condition, *conditions)
51
+ validate_condition(first_condition)
52
+ validate_condition(second_condition)
53
+ combined = first_condition.and(second_condition)
54
+
55
+ unless conditions.blank?
56
+ conditions.each do |condition|
57
+ combined = combined.and(condition)
58
+ end
59
+ end
60
+
61
+ combined
62
+ end
63
+
64
+ # Join conditions using not.
65
+ # @param [Arel::Nodes::Node] condition
66
+ # @return [Arel::Nodes::Node] condition
67
+ def compose_not(condition)
68
+ validate_condition(condition)
69
+ condition.not
70
+ end
71
+
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,268 @@
1
+ module Clearly
2
+ module Query
3
+ module Compose
4
+
5
+ # Public class for creating custom queries.
6
+ class Custom
7
+ include Clearly::Query::Compose::Comparison
8
+ include Clearly::Query::Compose::Core
9
+ include Clearly::Query::Compose::Range
10
+ include Clearly::Query::Compose::Subset
11
+ include Clearly::Query::Compose::Special
12
+ include Clearly::Query::Validate
13
+
14
+ # Create equals condition.
15
+ # @param [Arel::Table] table
16
+ # @param [Symbol] column_name
17
+ # @param [Array<Symbol>] allowed
18
+ # @param [Object] value
19
+ # @return [Arel::Nodes::Node] condition
20
+ def compose_eq(table, column_name, allowed, value)
21
+ validate_table_column(table, column_name, allowed)
22
+ compose_eq_node(table[column_name], value)
23
+ end
24
+
25
+ # Create not equals condition.
26
+ # @param [Arel::Table] table
27
+ # @param [Symbol] column_name
28
+ # @param [Array<Symbol>] allowed
29
+ # @param [Object] value
30
+ # @return [Arel::Nodes::Node] condition
31
+ def compose_not_eq(table, column_name, allowed, value)
32
+ validate_table_column(table, column_name, allowed)
33
+ compose_not_eq_node(table[column_name], value)
34
+ end
35
+
36
+ # Create less than condition.
37
+ # @param [Arel::Table] table
38
+ # @param [Symbol] column_name
39
+ # @param [Array<Symbol>] allowed
40
+ # @param [Object] value
41
+ # @return [Arel::Nodes::Node] condition
42
+ def compose_lt(table, column_name, allowed, value)
43
+ validate_table_column(table, column_name, allowed)
44
+ compose_lt_node(table[column_name], value)
45
+ end
46
+
47
+ # Create not less than condition.
48
+ # @param [Arel::Table] table
49
+ # @param [Symbol] column_name
50
+ # @param [Array<Symbol>] allowed
51
+ # @param [Object] value
52
+ # @return [Arel::Nodes::Node] condition
53
+ def compose_not_lt(table, column_name, allowed, value)
54
+ compose_gteq(table, column_name, allowed, value)
55
+ end
56
+
57
+ # Create greater than condition.
58
+ # @param [Arel::Table] table
59
+ # @param [Symbol] column_name
60
+ # @param [Array<Symbol>] allowed
61
+ # @param [Object] value
62
+ # @return [Arel::Nodes::Node] condition
63
+ def compose_gt(table, column_name, allowed, value)
64
+ validate_table_column(table, column_name, allowed)
65
+ compose_gt_node(table[column_name], value)
66
+ end
67
+
68
+ # Create not greater than condition.
69
+ # @param [Arel::Table] table
70
+ # @param [Symbol] column_name
71
+ # @param [Array<Symbol>] allowed
72
+ # @param [Object] value
73
+ # @return [Arel::Nodes::Node] condition
74
+ def compose_not_gt(table, column_name, allowed, value)
75
+ compose_lteq(table, column_name, allowed, value)
76
+ end
77
+
78
+ # Create less than or equal condition.
79
+ # @param [Arel::Table] table
80
+ # @param [Symbol] column_name
81
+ # @param [Array<Symbol>] allowed
82
+ # @param [Object] value
83
+ # @return [Arel::Nodes::Node] condition
84
+ def compose_lteq(table, column_name, allowed, value)
85
+ validate_table_column(table, column_name, allowed)
86
+ compose_lteq_node(table[column_name], value)
87
+ end
88
+
89
+ # Create not less than or equal condition.
90
+ # @param [Arel::Table] table
91
+ # @param [Symbol] column_name
92
+ # @param [Array<Symbol>] allowed
93
+ # @param [Object] value
94
+ # @return [Arel::Nodes::Node] condition
95
+ def compose_not_lteq(table, column_name, allowed, value)
96
+ compose_gt(table, column_name, allowed, value)
97
+ end
98
+
99
+ # Create greater than or equal condition.
100
+ # @param [Arel::Table] table
101
+ # @param [Symbol] column_name
102
+ # @param [Array<Symbol>] allowed
103
+ # @param [Object] value
104
+ # @return [Arel::Nodes::Node] condition
105
+ def compose_gteq(table, column_name, allowed, value)
106
+ validate_table_column(table, column_name, allowed)
107
+ compose_gteq_node(table[column_name], value)
108
+ end
109
+
110
+ # Create not greater than or equal condition.
111
+ # @param [Arel::Table] table
112
+ # @param [Symbol] column_name
113
+ # @param [Array<Symbol>] allowed
114
+ # @param [Object] value
115
+ # @return [Arel::Nodes::Node] condition
116
+ def compose_not_gteq(table, column_name, allowed, value)
117
+ compose_lt(table, column_name, allowed, value)
118
+ end
119
+
120
+ # Create contains condition.
121
+ # @param [Arel::Table] table
122
+ # @param [Symbol] column_name
123
+ # @param [Array<Symbol>] allowed
124
+ # @param [Object] value
125
+ # @return [Arel::Nodes::Node] condition
126
+ def compose_contains(table, column_name, allowed, value)
127
+ validate_table_column(table, column_name, allowed)
128
+ compose_contains_node(table[column_name], value)
129
+ end
130
+
131
+ # Create not contains condition.
132
+ # @param [Arel::Table] table
133
+ # @param [Symbol] column_name
134
+ # @param [Array<Symbol>] allowed
135
+ # @param [Object] value
136
+ # @return [Arel::Nodes::Node] condition
137
+ def compose_not_contains(table, column_name, allowed, value)
138
+ validate_table_column(table, column_name, allowed)
139
+ compose_not_contains_node(table[column_name], value)
140
+ end
141
+
142
+ # Create starts_with condition.
143
+ # @param [Arel::Table] table
144
+ # @param [Symbol] column_name
145
+ # @param [Array<Symbol>] allowed
146
+ # @param [Object] value
147
+ # @return [Arel::Nodes::Node] condition
148
+ def compose_starts_with(table, column_name, allowed, value)
149
+ validate_table_column(table, column_name, allowed)
150
+ compose_starts_with_node(table[column_name], value)
151
+ end
152
+
153
+ # Create not starts_with condition.
154
+ # @param [Arel::Table] table
155
+ # @param [Symbol] column_name
156
+ # @param [Array<Symbol>] allowed
157
+ # @param [Object] value
158
+ # @return [Arel::Nodes::Node] condition
159
+ def compose_not_starts_with(table, column_name, allowed, value)
160
+ validate_table_column(table, column_name, allowed)
161
+ compose_not_starts_with_node(table[column_name], value)
162
+ end
163
+
164
+ # Create ends_with condition.
165
+ # @param [Arel::Table] table
166
+ # @param [Symbol] column_name
167
+ # @param [Array<Symbol>] allowed
168
+ # @param [Object] value
169
+ # @return [Arel::Nodes::Node] condition
170
+ def compose_ends_with(table, column_name, allowed, value)
171
+ validate_table_column(table, column_name, allowed)
172
+ compose_ends_with_node(table[column_name], value)
173
+ end
174
+
175
+ # Create not ends_with condition.
176
+ # @param [Arel::Table] table
177
+ # @param [Symbol] column_name
178
+ # @param [Array<Symbol>] allowed
179
+ # @param [Object] value
180
+ # @return [Arel::Nodes::Node] condition
181
+ def compose_not_ends_with(table, column_name, allowed, value)
182
+ validate_table_column(table, column_name, allowed)
183
+ compose_not_ends_with_node(table[column_name], value)
184
+ end
185
+
186
+ # Create IN condition.
187
+ # @param [Arel::Table] table
188
+ # @param [Symbol] column_name
189
+ # @param [Array<Symbol>] allowed
190
+ # @param [Array] values
191
+ # @return [Arel::Nodes::Node] condition
192
+ def compose_in(table, column_name, allowed, values)
193
+ validate_table_column(table, column_name, allowed)
194
+ compose_in_node(table[column_name], values)
195
+ end
196
+
197
+ # Create NOT IN condition.
198
+ # @param [Arel::Table] table
199
+ # @param [Symbol] column_name
200
+ # @param [Array<Symbol>] allowed
201
+ # @param [Array] values
202
+ # @return [Arel::Nodes::Node] condition
203
+ def compose_not_in(table, column_name, allowed, values)
204
+ validate_table_column(table, column_name, allowed)
205
+ compose_not_in_node(table[column_name], values)
206
+ end
207
+
208
+ # Create regular expression condition.
209
+ # @param [Arel::Table] table
210
+ # @param [Symbol] column_name
211
+ # @param [Array<Symbol>] allowed
212
+ # @param [Object] value
213
+ # @return [Arel::Nodes::Node] condition
214
+ def compose_regex(table, column_name, allowed, value)
215
+ validate_table_column(table, column_name, allowed)
216
+ compose_regex_node(table[column_name], value)
217
+ end
218
+
219
+ # Create negated regular expression condition.
220
+ # Not available just now, maybe in Arel 6?
221
+ # @param [Arel::Table] table
222
+ # @param [Symbol] column_name
223
+ # @param [Array<Symbol>] allowed
224
+ # @param [Object] value
225
+ # @return [Arel::Nodes::Node] condition
226
+ def compose_not_regex(table, column_name, allowed, value)
227
+ validate_table_column(table, column_name, allowed)
228
+ compose_not_regex_node(table[column_name], value)
229
+ end
230
+
231
+ # Create null comparison.
232
+ # @param [Arel::Table] table
233
+ # @param [Symbol] column_name
234
+ # @param [Array<Symbol>] allowed
235
+ # @param [Boolean] value
236
+ # @return [Arel::Nodes::Node] condition
237
+ def compose_null(table, column_name, allowed, value)
238
+ validate_table_column(table, column_name, allowed)
239
+ validate_boolean(value)
240
+ compose_null_node(table[column_name], value)
241
+ end
242
+
243
+ # Create IN condition using from (inclusive) and to (exclusive).
244
+ # @param [Arel::Table] table
245
+ # @param [Symbol] column_name
246
+ # @param [Array<Symbol>] allowed
247
+ # @param [Object] value
248
+ # @return [Arel::Nodes::Node] condition
249
+ def compose_range(table, column_name, allowed, value)
250
+ validate_table_column(table, column_name, allowed)
251
+ compose_range_node(table[column_name], value)
252
+ end
253
+
254
+ # Create NOT IN condition using from (inclusive) and to (exclusive).
255
+ # @param [Arel::Table] table
256
+ # @param [Symbol] column_name
257
+ # @param [Array<Symbol>] allowed
258
+ # @param [Object] value
259
+ # @return [Arel::Nodes::Node] condition
260
+ def compose_not_range(table, column_name, allowed, value)
261
+ validate_table_column(table, column_name, allowed)
262
+ compose_not_range_node(table[column_name], value)
263
+ end
264
+
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,114 @@
1
+ module Clearly
2
+ module Query
3
+ module Compose
4
+
5
+ # Methods for composing range queries.
6
+ module Range
7
+ include Clearly::Query::Validate
8
+
9
+ # Parse an interval.
10
+ # @param [String] value
11
+ # @return [Array<String>] captures
12
+ def parse_interval(value)
13
+ range_regex = /(\[|\()(.*),(.*)(\)|\])/i
14
+ matches = value.match(range_regex)
15
+ fail Clearly::Query::QueryArgumentError.new(
16
+ "range string must be in the form (|[.*,.*]|), got '#{value}'") unless matches
17
+
18
+ captures = matches.captures
19
+ {
20
+ start_include: captures[0] == '[',
21
+ start_value: captures[1],
22
+ end_value: captures[2],
23
+ end_include: captures[3] == ']'
24
+ }
25
+ end
26
+
27
+ # Validate a range.
28
+ # @param [Hash] hash
29
+ # @return [Hash]
30
+ def parse_range(hash)
31
+ unless hash.is_a?(Hash)
32
+ fail Clearly::Query::QueryArgumentError.new(
33
+ "range filter must be {'from': 'value', 'to': 'value'} " +
34
+ "or {'interval': '(|[.*,.*]|)'} got '#{hash}'", {hash: hash})
35
+
36
+ end
37
+
38
+ from = hash[:from]
39
+ to = hash[:to]
40
+ interval = hash[:interval]
41
+
42
+ if !from.blank? && !to.blank? && !interval.blank?
43
+ fail Clearly::Query::QueryArgumentError.new(
44
+ "range filter must use either ('from' and 'to') or ('interval'), not both", {hash: hash})
45
+ elsif from.blank? && !to.blank?
46
+ fail Clearly::Query::QueryArgumentError.new(
47
+ "range filter missing 'from'", {hash: hash})
48
+ elsif !from.blank? && to.blank?
49
+ fail Clearly::Query::QueryArgumentError.new(
50
+ "range filter missing 'to'", {hash: hash})
51
+ elsif !from.blank? && !to.blank?
52
+ parse_interval("[#{from},#{to})")
53
+ elsif !interval.blank?
54
+ parse_interval(interval)
55
+ else
56
+ fail Clearly::Query::QueryArgumentError.new(
57
+ "range filter did not contain ('from' and 'to') or ('interval'), got '#{hash}'", {hash: hash})
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # Create IN condition using from (inclusive) and to (exclusive).
64
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
65
+ # @param [Object] value
66
+ # @return [Arel::Nodes::Node] condition
67
+ def compose_range_node(node, value)
68
+ validate_node_or_attribute(node)
69
+ range_info = parse_range(value)
70
+
71
+ # build using gt, lt, gteq, lteq
72
+ if range_info[:start_include]
73
+ start_condition = node.gteq(range_info[:start_value])
74
+ else
75
+ start_condition = node.gt(range_info[:start_value])
76
+ end
77
+
78
+ if range_info[:end_include]
79
+ end_condition = node.lteq(range_info[:end_value])
80
+ else
81
+ end_condition = node.lt(range_info[:end_value])
82
+ end
83
+
84
+ start_condition.and(end_condition)
85
+ end
86
+
87
+ # Create NOT IN condition using from (inclusive) and to (exclusive).
88
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
89
+ # @param [Object] value
90
+ # @return [Arel::Nodes::Node] condition
91
+ def compose_not_range_node(node, value)
92
+ validate_node_or_attribute(node)
93
+ range_info = parse_range(value)
94
+
95
+ # build using gt, lt, gteq, lteq
96
+ if range_info[:start_include]
97
+ start_condition = node.lt(range_info[:start_value])
98
+ else
99
+ start_condition = node.lteq(range_info[:start_value])
100
+ end
101
+
102
+ if range_info[:end_include]
103
+ end_condition = node.gt(range_info[:end_value])
104
+ else
105
+ end_condition = node.gteq(range_info[:end_value])
106
+ end
107
+
108
+ start_condition.or(end_condition)
109
+ end
110
+
111
+ end
112
+ end
113
+ end
114
+ end