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.
- data/lib/ext/array.rb +10 -0
- data/lib/ext/hash.rb +10 -0
- data/lib/ext/string.rb +10 -0
- data/lib/hash_to_conditions.rb +121 -0
- data/lib/helpers/array_helper.rb +69 -0
- data/lib/helpers/hash_helper.rb +85 -0
- data/lib/helpers/string_helper.rb +93 -0
- metadata +52 -0
data/lib/ext/array.rb
ADDED
data/lib/ext/hash.rb
ADDED
data/lib/ext/string.rb
ADDED
@@ -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: []
|