hash-to-conditions 0.3.3

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.
@@ -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: []