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
data/bin/yard ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This file was generated by Bundler.
4
+ #
5
+ # The application 'yard' is installed as part of a gem, and
6
+ # this file is here to facilitate running it.
7
+ #
8
+
9
+ require 'pathname'
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
11
+ Pathname.new(__FILE__).realpath)
12
+
13
+ require 'rubygems'
14
+ require 'bundler/setup'
15
+
16
+ load Gem.bin_path('yard', 'yard')
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'clearly/query/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'clearly-query'
8
+ spec.version = Clearly::Query::VERSION
9
+ spec.authors = ['@cofiem']
10
+ spec.email = ['cofiem@gmail.com']
11
+ spec.summary = %q{A library for constructing an sql query from a hash.}
12
+ spec.description = %q{A library for constructing an sql query from a hash. Uses a strict, yet flexible specification.}
13
+ spec.homepage = 'https://github.com/cofiem/clearly-query'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_runtime_dependency 'arel', '~> 6'
22
+ spec.add_runtime_dependency 'activesupport', '~> 4'
23
+ spec.add_runtime_dependency 'activerecord', '~> 4'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1'
26
+ spec.add_development_dependency 'rake', '~> 10'
27
+ spec.add_development_dependency 'guard-rspec', '~> 4'
28
+ spec.add_development_dependency 'guard-yard', '~> 2'
29
+ spec.add_development_dependency 'simplecov', '~> 0'
30
+ spec.add_development_dependency 'sqlite3', '~> 1'
31
+ spec.add_development_dependency 'zonebie', '~> 0'
32
+ spec.add_development_dependency 'database_cleaner', '~> 1'
33
+ end
@@ -0,0 +1,22 @@
1
+ require 'active_support/all'
2
+
3
+ require 'clearly/query/version'
4
+
5
+ require 'clearly/query/errors'
6
+ require 'clearly/query/helper'
7
+ require 'clearly/query/validate'
8
+ require 'clearly/query/cleaner'
9
+
10
+ require 'clearly/query/compose/core'
11
+ require 'clearly/query/compose/subset'
12
+ require 'clearly/query/compose/comparison'
13
+ require 'clearly/query/compose/range'
14
+ require 'clearly/query/compose/special'
15
+ require 'clearly/query/compose/custom'
16
+ require 'clearly/query/compose/conditions'
17
+
18
+ require 'clearly/query/composer'
19
+ require 'clearly/query/definition'
20
+ require 'clearly/query/graph'
21
+
22
+
@@ -0,0 +1,63 @@
1
+ module Clearly
2
+ module Query
3
+
4
+ # Cleans a filter hash so it is ready to be built using the Composer.
5
+ class Cleaner
6
+
7
+ # Create a cleaner for a filter hash.
8
+ # @return [Clearly::Query::Cleaner]
9
+ def initialize
10
+ self
11
+ end
12
+
13
+ # Get the cleaned filter hash.
14
+ # @param [Hash] hash
15
+ # @return [Hash]
16
+ def do(hash)
17
+ clean(hash)
18
+ end
19
+
20
+ private
21
+
22
+ # Clean an object.
23
+ # @param [Object] value
24
+ # @return [Object]
25
+ def clean(value)
26
+ if value.is_a?(Hash)
27
+ clean_hash(value)
28
+ elsif value.is_a?(Array)
29
+ clean_array(value)
30
+ else
31
+ value
32
+ end
33
+ end
34
+
35
+ # Clean a hash.
36
+ # @param [Hash] hash
37
+ # @return [Hash] Cleaned hash
38
+ def clean_hash(hash)
39
+ cleaned_hash = Hash.new
40
+ hash.each do |key, value|
41
+ new_key = clean_value(key)
42
+ cleaned_hash[new_key] = clean(value)
43
+ end
44
+ cleaned_hash
45
+ end
46
+
47
+ # Clean an array.
48
+ # @param [Array] array
49
+ # @return [Array]
50
+ def clean_array(array)
51
+ array.map { |item| clean(item) }
52
+ end
53
+
54
+ # Convert to a snake case symbol
55
+ # @param [Object] value
56
+ # @return [Symbol]
57
+ def clean_value(value)
58
+ value.to_s.underscore.to_sym
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,102 @@
1
+ module Clearly
2
+ module Query
3
+
4
+ # Arel helper methods used by Composer and Definition
5
+ module Compose
6
+
7
+ # Provides comparisons for composing queries.
8
+ module Comparison
9
+ include Clearly::Query::Validate
10
+
11
+ private
12
+
13
+ # Create equals condition.
14
+ # @param [Arel::Nodes::Node] node
15
+ # @param [Object] value
16
+ # @return [Arel::Nodes::Node] condition
17
+ def compose_eq_node(node, value)
18
+ validate_node_or_attribute(node)
19
+ node.eq(value)
20
+ end
21
+
22
+ # Create not equals condition.
23
+ # @param [Arel::Nodes::Node] node
24
+ # @param [Object] value
25
+ # @return [Arel::Nodes::Node] condition
26
+ def compose_not_eq_node(node, value)
27
+ validate_node_or_attribute(node)
28
+ node.not_eq(value)
29
+ end
30
+
31
+ # Create less than condition.
32
+ # @param [Arel::Nodes::Node] node
33
+ # @param [Object] value
34
+ # @return [Arel::Nodes::Node] condition
35
+ def compose_lt_node(node, value)
36
+ validate_node_or_attribute(node)
37
+ node.lt(value)
38
+ end
39
+
40
+ # Create not less than condition.
41
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
42
+ # @param [Object] value
43
+ # @return [Arel::Nodes::Node] condition
44
+ def compose_not_lt_node(node, value)
45
+ compose_gteq_node(node, value)
46
+ end
47
+
48
+ # Create greater than condition.
49
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
50
+ # @param [Object] value
51
+ # @return [Arel::Nodes::Node] condition
52
+ def compose_gt_node(node, value)
53
+ validate_node_or_attribute(node)
54
+ node.gt(value)
55
+ end
56
+
57
+ # Create not greater than condition.
58
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
59
+ # @param [Object] value
60
+ # @return [Arel::Nodes::Node] condition
61
+ def compose_not_gt_node(node, value)
62
+ compose_lteq_node(node, value)
63
+ end
64
+
65
+ # Create less than or equal condition.
66
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
67
+ # @param [Object] value
68
+ # @return [Arel::Nodes::Node] condition
69
+ def compose_lteq_node(node, value)
70
+ validate_node_or_attribute(node)
71
+ node.lteq(value)
72
+ end
73
+
74
+ # Create not less than or equal condition.
75
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
76
+ # @param [Object] value
77
+ # @return [Arel::Nodes::Node] condition
78
+ def compose_not_lteq_node(node, value)
79
+ compose_gt_node(node, value)
80
+ end
81
+
82
+ # Create greater than or equal condition.
83
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
84
+ # @param [Object] value
85
+ # @return [Arel::Nodes::Node] condition
86
+ def compose_gteq_node(node, value)
87
+ validate_node_or_attribute(node)
88
+ node.gteq(value)
89
+ end
90
+
91
+ # Create not greater than or equal condition.
92
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
93
+ # @param [Object] value
94
+ # @return [Arel::Nodes::Node] condition
95
+ def compose_not_gteq_node(node, value)
96
+ compose_lt_node(node, value)
97
+ end
98
+
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,215 @@
1
+ module Clearly
2
+ module Query
3
+ module Compose
4
+
5
+ # Methods for building conditions.
6
+ module Conditions
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
+ # Logical query operators
15
+ OPERATORS_LOGICAL = [:and, :or, :not]
16
+
17
+ # Comparison query operators
18
+ OPERATORS_COMPARISON = [
19
+ :eq, :equal,
20
+ :not_eq, :not_equal,
21
+ :lt, :less_than,
22
+ :not_lt, :not_less_than,
23
+ :gt, :greater_than,
24
+ :not_gt, :not_greater_than,
25
+ :lteq, :less_than_or_equal,
26
+ :not_lteq, :not_less_than_or_equal,
27
+ :gteq, :greater_than_or_equal,
28
+ :not_gteq, :not_greater_than_or_equal]
29
+
30
+ # range query operators
31
+ OPERATORS_RANGE = [
32
+ :range, :in_range,
33
+ :not_range, :not_in_range]
34
+
35
+ # Subset query operators
36
+ OPERATORS_SUBSET = [
37
+ :in, :is_in,
38
+ :not_in, :is_not_in,
39
+ :contains, :contain,
40
+ :not_contains, :not_contain, :does_not_contain,
41
+ :starts_with, :start_with,
42
+ :not_starts_with, :not_start_with, :does_not_start_with,
43
+ :ends_with, :end_with,
44
+ :not_ends_with, :not_end_with, :does_not_end_with]
45
+
46
+ # Regex query operators
47
+ OPERATORS_REGEX = [
48
+ :regex, :regex_match, :matches,
49
+ :not_regex, :not_regex_match, :does_not_match, :not_match]
50
+
51
+ # Special query operators (null)
52
+ OPERATORS_SPECIAL = [:null, :is_null]
53
+
54
+ # All query operators except logical operators
55
+ # All query operators except logical operators
56
+ OPERATORS =
57
+ OPERATORS_COMPARISON +
58
+ OPERATORS_RANGE +
59
+ OPERATORS_SUBSET +
60
+ OPERATORS_REGEX +
61
+ OPERATORS_SPECIAL
62
+
63
+ # Add conditions to a query.
64
+ # @param [ActiveRecord::Relation] query
65
+ # @param [Array<Arel::Nodes::Node>, Arel::Nodes::Node] conditions
66
+ # @return [ActiveRecord::Relation] the modified query
67
+ def condition_apply(query, conditions)
68
+ conditions = [conditions].flatten
69
+ validate_not_blank(conditions)
70
+ validate_array(conditions)
71
+
72
+ conditions.each do |condition|
73
+ validate_condition(condition)
74
+ query = query.where(condition)
75
+ end
76
+
77
+ query
78
+ end
79
+
80
+ # Combine multiple conditions.
81
+ # @param [Symbol] combiner
82
+ # @param [Arel::Nodes::Node, Array<Arel::Nodes::Node>] conditions
83
+ # @return [Arel::Nodes::Node] condition
84
+ def condition_combine(combiner, *conditions)
85
+ conditions = [conditions].flatten
86
+ validate_not_blank(conditions)
87
+ validate_array(conditions)
88
+ validate_condition(conditions[0])
89
+ validate_array_items(conditions)
90
+
91
+ combined_conditions = nil
92
+ conditions.each do |condition|
93
+ combined_conditions = condition_combine_new(combiner, combined_conditions, condition)
94
+ end
95
+ combined_conditions
96
+ end
97
+
98
+ # Combine multiple conditions with a new condition.
99
+ # @param [Symbol] combiner
100
+ # @param [Arel::Nodes::Node, Array<Arel::Nodes::Node>] conditions
101
+ # @param [Arel::Nodes::Node] new_condition
102
+ # @return [Arel::Nodes::Node] condition
103
+ def condition_combine_new(combiner, conditions, new_condition)
104
+ case combiner
105
+ when :and
106
+ conditions.nil? ? new_condition : compose_and(conditions, new_condition)
107
+ when :or
108
+ conditions.nil? ? new_condition : compose_or(conditions, new_condition)
109
+ when :not
110
+ not_condition = compose_not(new_condition)
111
+ conditions.nil? ? not_condition : compose_and(conditions, not_condition)
112
+ else
113
+ fail Clearly::Query::QueryArgumentError.new("unrecognised logical operator '#{combiner}'")
114
+ end
115
+ end
116
+
117
+
118
+ # Build a condition.
119
+ # @param [Symbol] operator
120
+ # @param [Arel::Table] table
121
+ # @param [Symbol] column_name
122
+ # @param [Array<symbol>] valid_fields
123
+ # @param [Object] value
124
+ # @return [Arel::Nodes::Node] condition
125
+ def condition_components(operator, table, column_name, valid_fields, value)
126
+ validate_table_column(table, column_name, valid_fields)
127
+ condition_node(operator, table[column_name], value)
128
+ end
129
+
130
+ # Build a condition.
131
+ # @param [Symbol] operator
132
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
133
+ # @param [Object] value
134
+ # @return [Arel::Nodes::Node] condition
135
+ def condition_node(operator, node, value)
136
+ new_condition = condition_node_comparison(operator, node, value)
137
+ new_condition = condition_node_subset(operator, node, value) if new_condition.nil?
138
+ new_condition = compose_null_node(node, value) if new_condition.nil? && [:null, :is_null].include?(operator)
139
+
140
+ fail Clearly::Query::QueryArgumentError.new("unrecognised operator '#{operator}'") if new_condition.nil?
141
+ new_condition
142
+ end
143
+
144
+ # Build a comparison condition.
145
+ # @param [Symbol] operator
146
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
147
+ # @param [Object] value
148
+ # @return [Arel::Nodes::Node] condition
149
+ def condition_node_comparison(operator, node, value)
150
+ case operator
151
+ when :eq, :equal
152
+ compose_eq_node(node, value)
153
+ when :not_eq, :not_equal
154
+ compose_not_eq_node(node, value)
155
+ when :lt, :less_than
156
+ compose_lt_node(node, value)
157
+ when :not_lt, :not_less_than
158
+ compose_not_lt_node(node, value)
159
+ when :gt, :greater_than
160
+ compose_gt_node(node, value)
161
+ when :not_gt, :not_greater_than
162
+ compose_not_gt_node(node, value)
163
+ when :lteq, :less_than_or_equal
164
+ compose_lteq_node(node, value)
165
+ when :not_lteq, :not_less_than_or_equal
166
+ compose_not_lteq_node(node, value)
167
+ when :gteq, :greater_than_or_equal
168
+ compose_gteq_node(node, value)
169
+ when :not_gteq, :not_greater_than_or_equal
170
+ compose_not_gteq_node(node, value)
171
+ else
172
+ nil
173
+ end
174
+ end
175
+
176
+ # Build a range or subset condition.
177
+ # @param [Symbol] operator
178
+ # @param [Arel::Nodes::Node, Arel::Attributes::Attribute, String] node
179
+ # @param [Object] value
180
+ # @return [Arel::Nodes::Node] condition
181
+ def condition_node_subset(operator, node, value)
182
+ case operator
183
+ when :range, :in_range
184
+ compose_range_node(node, value)
185
+ when :not_range, :not_in_range
186
+ compose_not_range_node(node, value)
187
+ when :in, :is_in
188
+ compose_in_node(node, value)
189
+ when :not_in, :is_not_in
190
+ compose_not_in_node(node, value)
191
+ when :contains, :contain
192
+ compose_contains_node(node, value)
193
+ when :not_contains, :not_contain, :does_not_contain
194
+ compose_not_contains_node(node, value)
195
+ when :starts_with, :start_with
196
+ compose_starts_with_node(node, value)
197
+ when :not_starts_with, :not_start_with, :does_not_start_with
198
+ compose_not_starts_with_node(node, value)
199
+ when :ends_with, :end_with
200
+ compose_ends_with_node(node, value)
201
+ when :not_ends_with, :not_end_with, :does_not_end_with
202
+ compose_not_ends_with_node(node, value)
203
+ when :regex, :regex_match, :matches
204
+ compose_regex_node(node, value)
205
+ when :not_regex, :not_regex_match, :does_not_match, :not_match
206
+ compose_not_regex_node(node, value)
207
+ else
208
+ nil
209
+ end
210
+ end
211
+
212
+ end
213
+ end
214
+ end
215
+ end