hash-to-conditions 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|