hash-to-conditions 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ require 'helpers/array_helper'
2
+
3
+ class Array
4
+
5
+ # Extends Array with a convenience method to HashToConditions::ArrayHelper
6
+ def to_condition
7
+ HashToConditions::ArrayHelper.new(self).to_condition
8
+ end
9
+ end
10
+
@@ -0,0 +1,10 @@
1
+ require 'helpers/hash_helper'
2
+
3
+ class Hash
4
+
5
+ # Extends Hash with a convenience method to HashToConditions::HashHelper
6
+ def to_conditions
7
+ HashToConditions::HashHelper.new(self).to_conditions
8
+ end
9
+ end
10
+
@@ -0,0 +1,10 @@
1
+ require 'helpers/string_helper'
2
+
3
+ class String
4
+
5
+ # Extends String with a convenience method to HashToConditions::StringHelper
6
+ def to_operator
7
+ HashToConditions::StringHelper.new(self).to_operator
8
+ end
9
+ end
10
+
@@ -0,0 +1,121 @@
1
+ require 'ext/array'
2
+ require 'ext/hash'
3
+ require 'ext/string'
4
+
5
+ # The HashToConditions gem provides an easy way to build ActiveRecord Array conditions
6
+ # directly from a Hash.
7
+ #
8
+ # A simple example:
9
+ # > {'age.gt' => 18}.to_conditions => ['(age>?)', 18]
10
+ #
11
+ # The *age* field is marked up with the *.gt* operator tag, which is processed by the
12
+ # +to_conditions+ method in order to produce the result Array condition.
13
+ #
14
+ # More practical conditions can be constructed through the use of operator tags in combination
15
+ # with the *AND* and *OR* boolean operators.
16
+ #
17
+ # Below is a list of supported tags:
18
+ #
19
+ # <table style=\"border-collapse:collapse; border: 1px solid \#999\">
20
+ # <tr>
21
+ # <th style=\"border: 1px solid \#999; width: 80px\">Tag</th>
22
+ # <th style=\"border: 1px solid \#999; width: 150px\">SQL equivalent</th>
23
+ # </tr>
24
+ # <tr>
25
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">.eq</td>
26
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">=</td>
27
+ # </tr>
28
+ # <tr>
29
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">.ne</td>
30
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">\<\></td>
31
+ # </tr>
32
+ # <tr>
33
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">.gt</td>
34
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">\></td>
35
+ # </tr>
36
+ # <tr>
37
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">.ge</td>
38
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">\>=</td>
39
+ # </tr>
40
+ # <tr>
41
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">.lt</td>
42
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">\<</td>
43
+ # </tr>
44
+ # <tr>
45
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">.le</td>
46
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">\<=</td>
47
+ # </tr>
48
+ # <tr>
49
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">.like</td>
50
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">LIKE</td>
51
+ # </tr>
52
+ # <tr>
53
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">.null</td>
54
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">IS NULL</td>
55
+ # </tr>
56
+ # <tr>
57
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">.nnull</td>
58
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">IS NOT NULL</td>
59
+ # </tr>
60
+ # <tr>
61
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">.in</td>
62
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">IN</td>
63
+ # </tr>
64
+ # <tr>
65
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">.between</td>
66
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">BETWEEN</td>
67
+ # </tr>
68
+ # </table>
69
+ #
70
+ # Examples:
71
+ #
72
+ # > h = {'AND' => {'name.eq' => 'Ruby', 'version.ge' => 1.9}}
73
+ # > h.to_conditions => ['(name=? AND version>=?)', 'Ruby', 1.9]
74
+ #
75
+ # > h = {'OR' => {'name.like' => 'Lou%', 'age.gt' => 18}}
76
+ # > h.to_conditions => ['(name LIKE ? OR age>?)', 'Lou%', 18]
77
+ #
78
+ # Hash keys are not limited to String. Symbol keys are also supported so :'name.eq' is also valid.
79
+ #
80
+ # == Implicit Tags
81
+ #
82
+ # *.eq* and *.like* are implicit tags. Implicit means the tag can be omitted so the following are
83
+ # equivalent:
84
+ #
85
+ # > {'name.like' => 'Lou%'}.to_conditions => ['(name LIKE ?)', 'Lou%']
86
+ # > {'name' => 'Lou%'}.to_conditions => ['(name LIKE ?)', 'Lou%']
87
+ #
88
+ # Similarly,
89
+ #
90
+ # > {'name.eq' => 'Ruby', 'version.eq' => 1.9}.to_conditions => ["(name=? AND version=?)", "Ruby", 1.9]
91
+ # > {'name' => 'Ruby', 'version' => 1.9}.to_conditions => ["(name=? AND version=?)", "Ruby", 1.9]
92
+ #
93
+ # Note the *AND* boolean operator is omitted in the hashes above. It also is an implicit
94
+ # operator, except when used as a nested condition.
95
+ #
96
+ # Having these as implicits seem natural and can help save a few extra key-strokes.
97
+ #
98
+ # == Nested Conditions
99
+ #
100
+ # Nested conditions allow more complex queries to be constructed.
101
+ #
102
+ # For example:
103
+ #
104
+ # > nested_h = {'name' => 'Lou%', 'age.gt' => 18}
105
+ # > h = {'OR' => {'AND' => nested_h, 'salary.between' => '50k, 100k'}}
106
+ # > h.to_conditions => ['((name LIKE ? AND age>?) OR salary BETWEEN ? AND ?)', 'Lou%', 18, '50k', '100k']
107
+ #
108
+ # However, one can easily get into trouble nesting with hashes. It is easy to create many nestings or
109
+ # a cyclic one. Therefore nesting is limited to a maximum of 42 within a root hash. An
110
+ # exception with *nested_too_deep_or_cyclic* message is raised when this limit is exceeded.
111
+ #
112
+ # <br />
113
+ # ---
114
+ # Copyright (c) 2013 Long On, released under the MIT license
115
+
116
+ module HashToConditions
117
+
118
+ class HashToConditions
119
+ end
120
+
121
+ end
@@ -0,0 +1,69 @@
1
+ module HashToConditions
2
+
3
+ # This helper class takes a Hash key-value pair (an Array) and return the expanded form condition.
4
+ #
5
+ # For example:
6
+ # > helper = ArrayHelper.new(['age.gt', 18])
7
+ # > helper.to_condition => ['age>?', 18]
8
+
9
+ class ArrayHelper
10
+
11
+ # Returns an expanded form condition or a Hash if it is a nested condition.
12
+ #
13
+ # An exception is raised when specific validation fails. Possible exception messages are:
14
+ #
15
+ # * '*bad_key_value_pair*' - array must have exactly two (2) elements
16
+ # * '*field_cannot_be_empty*' - a field name must be specified
17
+ # * '*unknown_operator*' - operator tag is not supported
18
+ def to_condition
19
+ raise "bad_key_value_pair" unless @array.length == 2
20
+
21
+ parts = @array.first.to_s.split('.')
22
+ raise "field_cannot_be_empty" if parts.empty?
23
+
24
+ field = parts[0].strip
25
+ operator = parts[1]
26
+ value = @array.last
27
+
28
+ if ['AND', 'OR'].index(field.upcase)
29
+ # handle nested condition
30
+ return {field.upcase => value}
31
+ end
32
+
33
+ unless operator
34
+ # handle implicit .eq ({'name' => 'value'}) or .like ({'name' => 'value%'})
35
+ operator = value.to_s.index('%') ? 'like' : 'eq'
36
+ end
37
+ operator = operator.downcase
38
+ mapped = operator.to_operator
39
+ raise "unknown_operator" unless mapped
40
+
41
+ # handle .null or .nnull, suppress value
42
+ return [field + mapped] if operator.index('null')
43
+
44
+ # handle .in (?) or .between ? and ?
45
+ if ['in', 'between'].index(operator)
46
+ values = value.to_s.split(',').collect { | ea | ea.strip }
47
+ if 'in' == operator
48
+ result = [field + mapped, values]
49
+ else
50
+ # between
51
+ result = [field + mapped, values[0], values[1]]
52
+ end
53
+ return result
54
+ end
55
+
56
+ [field + mapped, value]
57
+ end
58
+
59
+
60
+ protected
61
+
62
+ # Creates a new instance
63
+ def initialize(array)
64
+ @array = array
65
+ end
66
+ end
67
+
68
+ end
69
+
@@ -0,0 +1,85 @@
1
+ module HashToConditions
2
+
3
+ # This class performs the bulk of the work. It takes a Hash and return the fully
4
+ # expanded Array condition.
5
+ #
6
+ # For example:
7
+ # > helper = HashHelper.new({'age.gt' => 18})
8
+ # > helper.to_conditions => ['(age>?)', 18]
9
+ #
10
+ # Boolean *AND* and *OR* can be used to join multiple conditions.
11
+ #
12
+ # Examples:
13
+ #
14
+ # > helper = HashHelper.new({'AND' => {'name' => 'Lou%', 'age.gt' => 18}})
15
+ # > helper.to_conditions => ['(name LIKE ? AND age>?)', 'Lou%', 18]
16
+ #
17
+ # > helper = HashHelper.new({'name.like' => 'Lou%', 'age.gt' => 18}) - boolean AND is implicit here
18
+ # > helper.to_conditions => ['(name LIKE ? AND age>?)', 'Lou%', 18]
19
+ #
20
+ # > helper = HashHelper.new({'OR' => {'name.like' => 'Lou%', 'age.gt' => 18}})
21
+ # > helper.to_conditions => ['(name LIKE ? OR age>?)', 'Lou%', 18]
22
+ #
23
+ # Nested conditions are also supported:
24
+ #
25
+ # > nested_h = {'name' => 'Lou%', 'age.gt' => 18}
26
+ # > helper = HashHelper.new({'OR' => {'AND' => nested_h, 'salary.between' => '50k, 100k'}})
27
+ # > helper.to_conditions => ['((name LIKE ? AND age>?) OR salary BETWEEN ? AND ?)', 'Lou%', 18, '50k', '100k']
28
+ #
29
+
30
+ class HashHelper
31
+
32
+ # Returns a fully expaned condition array
33
+ #
34
+ def to_conditions
35
+ raise "empty_condition" if @hash.empty?
36
+ result_s = ''
37
+ result_a = []
38
+ join_s = @hash.first.first
39
+ if ['AND', 'OR'].index(join_s.upcase)
40
+ parse(@hash.first.last, join_s.upcase, result_s, result_a)
41
+ else
42
+ parse(@hash, 'AND', result_s, result_a)
43
+ end
44
+ result_a.unshift(result_s)
45
+ result_a
46
+ end
47
+
48
+
49
+ protected
50
+
51
+ # Performs the translation. Parse @hash, construct the condition @result_s string using boolean
52
+ # operator @join_s. Collect @result_s and condition values in @result_a. An exception is raised,
53
+ # *nested_too_deep_or_cyclic*, when nesting exceeds 42 recursive calls.
54
+ def parse(hash, join_s, result_s, result_a, nest_lev=0)
55
+ raise "nested_too_deep_or_cyclic" unless nest_lev < 42
56
+
57
+ result_s << '('
58
+ count = hash.length
59
+ hash.each_pair { | pair |
60
+ arr = pair.to_condition
61
+ if arr.kind_of?(Hash)
62
+ # handle nested condition
63
+ parse(arr.first.last, arr.first.first, result_s, result_a, 1+nest_lev)
64
+ else
65
+ result_s << arr.shift
66
+ result_a.concat(arr)
67
+ end
68
+ count -= 1
69
+ if count > 0
70
+ result_s << ' '
71
+ result_s << join_s
72
+ result_s << ' '
73
+ end
74
+ }
75
+ result_s << ')'
76
+ end
77
+
78
+ # Creates a new instance
79
+ def initialize(hash)
80
+ @hash = hash
81
+ end
82
+ end
83
+
84
+ end
85
+
@@ -0,0 +1,93 @@
1
+ module HashToConditions
2
+
3
+ # This helper class takes an operator tag and convert it to an equivalent SQL operator.
4
+ #
5
+ # For example:
6
+ # > helper = StringHelper.new('null')
7
+ # > helper.to_operator => ' IS NULL'
8
+ #
9
+ # Supported operator tags:
10
+ #
11
+ # <table style=\"border-collapse:collapse; border: 1px solid \#999\">
12
+ # <tr>
13
+ # <th style=\"border: 1px solid \#999; width: 80px\">Tag</th>
14
+ # <th style=\"border: 1px solid \#999; width: 100px\">Output</th>
15
+ # </tr>
16
+ # <tr>
17
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">eq</td>
18
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">=</td>
19
+ # </tr>
20
+ # <tr>
21
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">ne</td>
22
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">\<\></td>
23
+ # </tr>
24
+ # <tr>
25
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">gt</td>
26
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">\></td>
27
+ # </tr>
28
+ # <tr>
29
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">ge</td>
30
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">\>=</td>
31
+ # </tr>
32
+ # <tr>
33
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">lt</td>
34
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">\<</td>
35
+ # </tr>
36
+ # <tr>
37
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">le</td>
38
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">\<=</td>
39
+ # </tr>
40
+ # <tr>
41
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">like</td>
42
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">LIKE</td>
43
+ # </tr>
44
+ # <tr>
45
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">null</td>
46
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">IS NULL</td>
47
+ # </tr>
48
+ # <tr>
49
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">nnull</td>
50
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">IS NOT NULL</td>
51
+ # </tr>
52
+ # <tr>
53
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">in</td>
54
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">IN</td>
55
+ # </tr>
56
+ # <tr>
57
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">between</td>
58
+ # <td style=\"border: 1px solid \#999; padding-left: 4px\">BETWEEN</td>
59
+ # </tr>
60
+ # </table>
61
+
62
+ class StringHelper
63
+ @@operators = {
64
+ 'eq' => '=?',
65
+ 'ne' => '<>?',
66
+ 'gt' => '>?',
67
+ 'ge' => '>=?',
68
+ 'lt' => '<?',
69
+ 'le' => '<=?',
70
+ 'like' => ' LIKE ?',
71
+ 'null' => ' IS NULL',
72
+ 'nnull' => ' IS NOT NULL',
73
+ 'in' => ' IN (?)',
74
+ 'between' => ' BETWEEN ? AND ?'
75
+ }
76
+
77
+
78
+ # Returns a matching SQL operator for @string, or nil if none found.
79
+ def to_operator
80
+ @@operators[@string.downcase]
81
+ end
82
+
83
+
84
+ protected
85
+
86
+ # Creates a new instance
87
+ def initialize(string)
88
+ @string = string
89
+ end
90
+ end
91
+
92
+ end
93
+
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hash-to-conditions
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.3
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Long On
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-31 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: The HashToConditions gem provides an easy way to build ActiveRecord Array
15
+ conditions directly from a Hash.
16
+ email: on.long.on@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - lib/hash_to_conditions.rb
22
+ - lib/helpers/string_helper.rb
23
+ - lib/helpers/array_helper.rb
24
+ - lib/helpers/hash_helper.rb
25
+ - lib/ext/array.rb
26
+ - lib/ext/string.rb
27
+ - lib/ext/hash.rb
28
+ homepage: https://rubygems.org/gems/hash-to-conditions
29
+ licenses: []
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ none: false
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ requirements: []
47
+ rubyforge_project:
48
+ rubygems_version: 1.8.24
49
+ signing_key:
50
+ specification_version: 3
51
+ summary: Converts a Hash to ActiveRecord Array conditions
52
+ test_files: []