surus 0.0.1 → 0.1.0
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/CHANGELOG.md +11 -0
- data/README.md +25 -3
- data/lib/surus.rb +5 -0
- data/lib/surus/array/decimal_serializer.rb +28 -0
- data/lib/surus/array/float_serializer.rb +28 -0
- data/lib/surus/array/integer_serializer.rb +28 -0
- data/lib/surus/array/scope.rb +15 -0
- data/lib/surus/array/text_serializer.rb +47 -0
- data/lib/surus/hstore/scope.rb +19 -17
- data/lib/surus/hstore/serializer.rb +133 -123
- data/lib/surus/version.rb +1 -1
- data/spec/array/decimal_serializer_spec.rb +23 -0
- data/spec/array/float_serializer_spec.rb +23 -0
- data/spec/array/integer_serializer_spec.rb +22 -0
- data/spec/array/scope_spec.rb +57 -0
- data/spec/array/text_serializer_spec.rb +29 -0
- data/spec/database.yml +1 -1
- data/spec/database_structure.sql +35 -0
- data/spec/hstore/scope_spec.rb +1 -1
- data/spec/hstore/serializer_spec.rb +3 -1
- data/spec/spec_helper.rb +18 -1
- metadata +23 -12
data/CHANGELOG.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# 0.1.0 (February 2, 2012)
|
2
|
+
|
3
|
+
* Added array serializers
|
4
|
+
* Added array scope helpers
|
5
|
+
* Moved everything into Surus namespace
|
6
|
+
* Add true and false to types that are successfully round-tripped as hstore keys and values
|
7
|
+
* Changed default test database name to surus_test
|
8
|
+
|
9
|
+
# 0.0.1 (January 31, 2012)
|
10
|
+
|
11
|
+
* Initial release
|
data/README.md
CHANGED
@@ -3,19 +3,23 @@ Surus
|
|
3
3
|
|
4
4
|
# Description
|
5
5
|
|
6
|
-
Surus extends ActiveRecord with PostgreSQL specific functionality.
|
7
|
-
|
6
|
+
Surus extends ActiveRecord with PostgreSQL specific functionality. It includes
|
7
|
+
hstore and array serializers and helper scopes.
|
8
8
|
|
9
9
|
# Installation
|
10
10
|
|
11
11
|
gem install surus
|
12
|
+
|
13
|
+
or add to your Gemfile
|
14
|
+
|
15
|
+
gem 'surus'
|
12
16
|
|
13
17
|
# Hstore
|
14
18
|
|
15
19
|
Hashes can be serialized to an hstore column.
|
16
20
|
|
17
21
|
class User < ActiveRecord::Base
|
18
|
-
serialize :properties, Hstore::Serializer.new
|
22
|
+
serialize :properties, Surus::Hstore::Serializer.new
|
19
23
|
end
|
20
24
|
|
21
25
|
Even though the underlying hstore can only use strings for keys and values
|
@@ -29,6 +33,24 @@ Hstores can be searched with helper scopes.
|
|
29
33
|
User.hstore_has_key(:properties, "favorite_color")
|
30
34
|
User.hstore_has_all_keys(:properties, "favorite_color", "gender")
|
31
35
|
User.hstore_has_any_keys(:properties, "favorite_color", "favorite_artist")
|
36
|
+
|
37
|
+
# Array
|
38
|
+
|
39
|
+
Ruby arrays can be serialized to PostgreSQL arrays. Surus includes support
|
40
|
+
for text, integer, float, and decimal arrays.
|
41
|
+
|
42
|
+
class User < ActiveRecord::Base
|
43
|
+
serialize :permissions, Surus::Array::TextSerializer.new
|
44
|
+
serialize :favorite_integers, Surus::Array::IntegerSerializer.new
|
45
|
+
serialize :favorite_floats, Surus::Array::FloatSerializer.new
|
46
|
+
serialize :favorite_decimals, Surus::Array::DecimalSerializer.new
|
47
|
+
end
|
48
|
+
|
49
|
+
Arrays can be searched with helper scopes.
|
50
|
+
|
51
|
+
User.array_has(:permissions, "admin")
|
52
|
+
User.array_has(:permissions, "manage_accounts", "manage_users")
|
53
|
+
User.array_has_any(:favorite_integers, 7, 11, 42)
|
32
54
|
|
33
55
|
# License
|
34
56
|
|
data/lib/surus.rb
CHANGED
@@ -2,3 +2,8 @@ require 'active_record'
|
|
2
2
|
require 'surus/version'
|
3
3
|
require 'surus/hstore/serializer'
|
4
4
|
require 'surus/hstore/scope'
|
5
|
+
require 'surus/array/text_serializer'
|
6
|
+
require 'surus/array/integer_serializer'
|
7
|
+
require 'surus/array/float_serializer'
|
8
|
+
require 'surus/array/decimal_serializer'
|
9
|
+
require 'surus/array/scope'
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Surus
|
2
|
+
module Array
|
3
|
+
class DecimalSerializer
|
4
|
+
ARRAY_REGEX = %r{
|
5
|
+
(?<=[\{,]) (?# All elements are prefixed with either the opening brace or a comma)
|
6
|
+
\-?\d+\.?\d*
|
7
|
+
|
|
8
|
+
NULL
|
9
|
+
}x
|
10
|
+
|
11
|
+
def load(string)
|
12
|
+
return unless string
|
13
|
+
string.scan(ARRAY_REGEX).map do |match|
|
14
|
+
match == "NULL" ? nil : BigDecimal(match)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def dump(array)
|
19
|
+
return unless array
|
20
|
+
'{' + array.map { |s| format(s) }.join(",") + '}'
|
21
|
+
end
|
22
|
+
|
23
|
+
def format(value)
|
24
|
+
value == nil ? "NULL" : value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Surus
|
2
|
+
module Array
|
3
|
+
class FloatSerializer
|
4
|
+
ARRAY_REGEX = %r{
|
5
|
+
(?<=[\{,]) (?# All elements are prefixed with either the opening brace or a comma)
|
6
|
+
\-?\d+(?:\.\d+)?(?:e[+-])?\d*
|
7
|
+
|
|
8
|
+
NULL
|
9
|
+
}x
|
10
|
+
|
11
|
+
def load(string)
|
12
|
+
return unless string
|
13
|
+
string.scan(ARRAY_REGEX).map do |match|
|
14
|
+
match == "NULL" ? nil : Float(match)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def dump(array)
|
19
|
+
return unless array
|
20
|
+
'{' + array.map { |s| format(s) }.join(",") + '}'
|
21
|
+
end
|
22
|
+
|
23
|
+
def format(value)
|
24
|
+
value == nil ? "NULL" : value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Surus
|
2
|
+
module Array
|
3
|
+
class IntegerSerializer
|
4
|
+
ARRAY_REGEX = %r{
|
5
|
+
(?<=[\{,]) (?# All elements are prefixed with either the opening brace or a comma)
|
6
|
+
\-?\d+
|
7
|
+
|
|
8
|
+
NULL
|
9
|
+
}x
|
10
|
+
|
11
|
+
def load(string)
|
12
|
+
return unless string
|
13
|
+
string.scan(ARRAY_REGEX).map do |match|
|
14
|
+
match == "NULL" ? nil : Integer(match)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def dump(array)
|
19
|
+
return unless array
|
20
|
+
'{' + array.map { |s| format(s) }.join(",") + '}'
|
21
|
+
end
|
22
|
+
|
23
|
+
def format(value)
|
24
|
+
value == nil ? "NULL" : value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Surus
|
2
|
+
module Array
|
3
|
+
module Scope
|
4
|
+
def array_has(column, *values)
|
5
|
+
where("#{connection.quote_column_name(column)} @> ARRAY[?]", values.flatten)
|
6
|
+
end
|
7
|
+
|
8
|
+
def array_has_any(column, *values)
|
9
|
+
where("#{connection.quote_column_name(column)} && ARRAY[?]", values.flatten)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
ActiveRecord::Base.extend Surus::Array::Scope
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Surus
|
2
|
+
module Array
|
3
|
+
class TextSerializer
|
4
|
+
ARRAY_REGEX = %r{
|
5
|
+
[{,] (?# All elements are prefixed with either the opening brace or a comma)
|
6
|
+
(?:
|
7
|
+
"
|
8
|
+
(?<quoted_string>(?:[^"\\]|\\.)*)
|
9
|
+
"
|
10
|
+
|
|
11
|
+
(?<null>NULL)
|
12
|
+
|
|
13
|
+
(?<unquoted_string>[^,}]+)
|
14
|
+
)
|
15
|
+
}x
|
16
|
+
|
17
|
+
def load(string)
|
18
|
+
return unless string
|
19
|
+
string.scan(ARRAY_REGEX).map do |quoted_string, null, unquoted_string|
|
20
|
+
element = quoted_string || unquoted_string
|
21
|
+
element ? unescape(element) : nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def dump(array)
|
26
|
+
return unless array
|
27
|
+
'{' + array.map { |s| format(s) }.join(",") + '}'
|
28
|
+
end
|
29
|
+
|
30
|
+
def format(value)
|
31
|
+
value == nil ? "NULL" : '"' + escape(value) + '"'
|
32
|
+
end
|
33
|
+
|
34
|
+
def escape(value)
|
35
|
+
value
|
36
|
+
.gsub('\\', '\\\\\\')
|
37
|
+
.gsub('"', '\\"')
|
38
|
+
end
|
39
|
+
|
40
|
+
def unescape(value)
|
41
|
+
value
|
42
|
+
.gsub('\\\\', '\\')
|
43
|
+
.gsub('\\"', '"')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/surus/hstore/scope.rb
CHANGED
@@ -1,21 +1,23 @@
|
|
1
|
-
module
|
2
|
-
module
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
1
|
+
module Surus
|
2
|
+
module Hstore
|
3
|
+
module Scope
|
4
|
+
def hstore_has_pairs(column, hash)
|
5
|
+
where("#{connection.quote_column_name(column)} @> ?", Serializer.new.dump(hash))
|
6
|
+
end
|
7
|
+
|
8
|
+
def hstore_has_key(column, key)
|
9
|
+
where("#{connection.quote_column_name(column)} ? :key", :key => key)
|
10
|
+
end
|
11
|
+
|
12
|
+
def hstore_has_all_keys(column, *keys)
|
13
|
+
where("#{connection.quote_column_name(column)} ?& ARRAY[:keys]", :keys => keys.flatten)
|
14
|
+
end
|
15
|
+
|
16
|
+
def hstore_has_any_keys(column, *keys)
|
17
|
+
where("#{connection.quote_column_name(column)} ?| ARRAY[:keys]", :keys => keys.flatten)
|
18
|
+
end
|
17
19
|
end
|
18
20
|
end
|
19
21
|
end
|
20
22
|
|
21
|
-
ActiveRecord::Base.extend Hstore::Scope
|
23
|
+
ActiveRecord::Base.extend Surus::Hstore::Scope
|
@@ -1,140 +1,150 @@
|
|
1
|
-
module
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
((?:[^"\\]|\\.)*)
|
6
|
-
"
|
7
|
-
=>
|
8
|
-
(
|
1
|
+
module Surus
|
2
|
+
module Hstore
|
3
|
+
class Serializer
|
4
|
+
KEY_VALUE_REGEX = %r{
|
9
5
|
"
|
10
|
-
(?:[^"\\]|\\.)*
|
6
|
+
((?:[^"\\]|\\.)*)
|
11
7
|
"
|
12
|
-
|
13
|
-
|
14
|
-
|
8
|
+
=>
|
9
|
+
(
|
10
|
+
"
|
11
|
+
(?:[^"\\]|\\.)*
|
12
|
+
"
|
13
|
+
|(NULL)
|
14
|
+
)
|
15
|
+
}x
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
17
|
+
def load(string)
|
18
|
+
return unless string
|
19
|
+
stringified_hash = string.scan(KEY_VALUE_REGEX).each_with_object({}) do |key_value, hash|
|
20
|
+
key, value = key_value
|
21
|
+
key = unescape(key)
|
22
|
+
value = if value == "NULL"
|
23
|
+
nil
|
24
|
+
else
|
25
|
+
unescape(value[1..-2])
|
26
|
+
end
|
27
|
+
|
28
|
+
hash[key] = value
|
25
29
|
end
|
30
|
+
|
31
|
+
key_types = stringified_hash.delete "__key_types"
|
32
|
+
key_types = YAML.load key_types if key_types
|
33
|
+
value_types = stringified_hash.delete "__value_types"
|
34
|
+
value_types = YAML.load value_types if value_types
|
26
35
|
|
27
|
-
|
36
|
+
return stringified_hash unless key_types || value_types
|
37
|
+
|
38
|
+
stringified_hash.each_with_object({}) do |key_value, hash|
|
39
|
+
string_key, string_value = key_value
|
40
|
+
|
41
|
+
key = if key_types && key_types.key?(string_key)
|
42
|
+
typecast(string_key, key_types[string_key])
|
43
|
+
else
|
44
|
+
string_key
|
45
|
+
end
|
46
|
+
|
47
|
+
value = if value_types && value_types.key?(string_key)
|
48
|
+
typecast(string_value, value_types[string_key])
|
49
|
+
else
|
50
|
+
string_value
|
51
|
+
end
|
52
|
+
|
53
|
+
hash[key] = value
|
54
|
+
end
|
28
55
|
end
|
29
|
-
|
30
|
-
key_types = stringified_hash.delete "__key_types"
|
31
|
-
key_types = YAML.load key_types if key_types
|
32
|
-
value_types = stringified_hash.delete "__value_types"
|
33
|
-
value_types = YAML.load value_types if value_types
|
34
56
|
|
35
|
-
|
36
|
-
|
37
|
-
stringified_hash.each_with_object({}) do |key_value, hash|
|
38
|
-
string_key, string_value = key_value
|
57
|
+
def dump(hash)
|
58
|
+
return unless hash
|
39
59
|
|
40
|
-
|
41
|
-
|
42
|
-
else
|
43
|
-
string_key
|
44
|
-
end
|
60
|
+
key_types = {}
|
61
|
+
value_types = {}
|
45
62
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
63
|
+
stringified_hash = hash.each_with_object({}) do |key_value, stringified_hash|
|
64
|
+
key_string, key_type = stringify(key_value[0])
|
65
|
+
value_string, value_type = stringify(key_value[1])
|
66
|
+
|
67
|
+
stringified_hash[key_string] = value_string
|
68
|
+
|
69
|
+
key_types[key_string] = key_type unless key_type == "String"
|
70
|
+
value_types[key_string] = value_type unless value_type == "String"
|
50
71
|
end
|
72
|
+
|
73
|
+
# Use YAML for recording types as it is much simpler than trying to
|
74
|
+
# handle all the special characters that could be in a key or value
|
75
|
+
# and encoding them again to fit into one string. Let YAML handle all
|
76
|
+
# the mess for us.
|
77
|
+
stringified_hash["__key_types"] = YAML.dump(key_types) if key_types.present?
|
78
|
+
stringified_hash["__value_types"] = YAML.dump(value_types) if value_types.present?
|
51
79
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
def dump(hash)
|
57
|
-
return unless hash
|
80
|
+
stringified_hash.map do |key, value|
|
81
|
+
"#{format_key(key)}=>#{format_value(value)}"
|
82
|
+
end.join(", ")
|
83
|
+
end
|
58
84
|
|
59
|
-
|
60
|
-
|
85
|
+
def format_key(key)
|
86
|
+
%Q("#{escape(key)}")
|
87
|
+
end
|
61
88
|
|
62
|
-
|
63
|
-
|
64
|
-
value_string, value_type = stringify(key_value[1])
|
65
|
-
|
66
|
-
stringified_hash[key_string] = value_string
|
67
|
-
|
68
|
-
key_types[key_string] = key_type unless key_type == "String"
|
69
|
-
value_types[key_string] = value_type unless value_type == "String"
|
89
|
+
def format_value(value)
|
90
|
+
value ? %Q("#{escape(value)}") : "NULL"
|
70
91
|
end
|
71
|
-
|
72
|
-
# Use YAML for recording types as it is much simpler than trying to
|
73
|
-
# handle all the special characters that could be in a key or value
|
74
|
-
# and encoding them again to fit into one string. Let YAML handle all
|
75
|
-
# the mess for us.
|
76
|
-
stringified_hash["__key_types"] = YAML.dump(key_types) if key_types.present?
|
77
|
-
stringified_hash["__value_types"] = YAML.dump(value_types) if value_types.present?
|
78
92
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
value
|
95
|
-
.
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
.
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
when "Date"
|
134
|
-
Date.parse(value)
|
135
|
-
else
|
136
|
-
raise ArgumentError, "Can't typecast: #{type}"
|
93
|
+
# Escape a value for use as a key or value in an hstore
|
94
|
+
def escape(value)
|
95
|
+
value
|
96
|
+
.gsub('\\', '\\\\\\')
|
97
|
+
.gsub('"', '\\"')
|
98
|
+
end
|
99
|
+
|
100
|
+
# Unescape a value from a key or value in an hstore
|
101
|
+
def unescape(value)
|
102
|
+
value
|
103
|
+
.gsub('\\\\', '\\')
|
104
|
+
.gsub('\\"', '"')
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns an array of value as a string and value type
|
108
|
+
def stringify(value)
|
109
|
+
if value.kind_of?(String)
|
110
|
+
[value, "String"]
|
111
|
+
elsif value.kind_of?(Integer)
|
112
|
+
[value.to_s, "Integer"]
|
113
|
+
elsif value.kind_of?(Float)
|
114
|
+
[value.to_s, "Float"]
|
115
|
+
elsif value.kind_of?(BigDecimal)
|
116
|
+
[value.to_s, "BigDecimal"]
|
117
|
+
elsif value.kind_of?(Date)
|
118
|
+
[value.to_s(:db), "Date"]
|
119
|
+
elsif value.kind_of?(TrueClass)
|
120
|
+
[value.to_s, "TrueClass"]
|
121
|
+
elsif value.kind_of?(FalseClass)
|
122
|
+
[value.to_s, "FalseClass"]
|
123
|
+
elsif value == nil
|
124
|
+
[nil, "String"] # we don't actually stringify nil because format_value special cases nil
|
125
|
+
else
|
126
|
+
[value.to_s, "String"] # coerce to string as we don't know how to reconstitue an unknown class
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def typecast(value, type)
|
131
|
+
case type
|
132
|
+
when "Integer"
|
133
|
+
Integer(value)
|
134
|
+
when "Float"
|
135
|
+
Float(value)
|
136
|
+
when "BigDecimal"
|
137
|
+
BigDecimal(value)
|
138
|
+
when "Date"
|
139
|
+
Date.parse(value)
|
140
|
+
when "TrueClass"
|
141
|
+
true
|
142
|
+
when "FalseClass"
|
143
|
+
false
|
144
|
+
else
|
145
|
+
raise ArgumentError, "Can't typecast: #{type}"
|
146
|
+
end
|
137
147
|
end
|
138
148
|
end
|
139
|
-
end
|
140
|
-
end
|
149
|
+
end
|
150
|
+
end
|
data/lib/surus/version.rb
CHANGED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Surus::Array::DecimalSerializer do
|
4
|
+
round_trip_examples = [
|
5
|
+
[nil, "nil"],
|
6
|
+
[[], "empty array"],
|
7
|
+
[[BigDecimal("0.0")], "single element"],
|
8
|
+
[[BigDecimal("1.0"), BigDecimal("2.0")], "multiple elements"],
|
9
|
+
[[BigDecimal("-1.0"), BigDecimal("-2.0")], "negative element"],
|
10
|
+
[[BigDecimal("1232493289348929843.323422349274382923")], "high magnitude element"],
|
11
|
+
[[BigDecimal("1.0"), BigDecimal("2.0"), BigDecimal("2.0")], "duplicated elements"],
|
12
|
+
[[BigDecimal("1.0"), nil], "an element is nil"],
|
13
|
+
[(10_000.times.map { |n| n * BigDecimal("1.13312") }).to_a, "huge array"]
|
14
|
+
]
|
15
|
+
|
16
|
+
round_trip_examples.each do |value, description|
|
17
|
+
it "round trips when #{description}" do
|
18
|
+
r = DecimalArrayRecord.create! :decimals => value
|
19
|
+
r.reload
|
20
|
+
r.decimals.should == value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Surus::Array::FloatSerializer do
|
4
|
+
round_trip_examples = [
|
5
|
+
[nil, "nil"],
|
6
|
+
[[], "empty array"],
|
7
|
+
[[0.0], "single element"],
|
8
|
+
[[1.0, 2.0], "multiple elements"],
|
9
|
+
[[-1.0, -2.0], "negative element"],
|
10
|
+
[[4.4325349e+45, 1.2324323e+77, 1.1242342e-57, 3e99], "high magnitude elements"],
|
11
|
+
[[1.0, 2.0, 2.0, 2.0], "duplicated elements"],
|
12
|
+
[[1.0, nil], "an element is nil"],
|
13
|
+
[(10_000.times.map { |n| n * 1.0 }).to_a, "huge array"]
|
14
|
+
]
|
15
|
+
|
16
|
+
round_trip_examples.each do |value, description|
|
17
|
+
it "round trips when #{description}" do
|
18
|
+
r = FloatArrayRecord.create! :floats => value
|
19
|
+
r.reload
|
20
|
+
r.floats.should == value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Surus::Array::IntegerSerializer do
|
4
|
+
round_trip_examples = [
|
5
|
+
[nil, "nil"],
|
6
|
+
[[], "empty array"],
|
7
|
+
[[0], "single element"],
|
8
|
+
[[1, 2], "multiple elements"],
|
9
|
+
[[-1, -2], "negative element"],
|
10
|
+
[[1, 2, 2, 2], "duplicated elements"],
|
11
|
+
[[1, nil], "an element is nil"],
|
12
|
+
[(1..10_000).to_a, "huge array"]
|
13
|
+
]
|
14
|
+
|
15
|
+
round_trip_examples.each do |value, description|
|
16
|
+
it "round trips when #{description}" do
|
17
|
+
r = IntegerArrayRecord.create! :integers => value
|
18
|
+
r.reload
|
19
|
+
r.integers.should == value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Surus::Array::Scope do
|
4
|
+
let!(:empty) { TextArrayRecord.create! :texts => [] }
|
5
|
+
|
6
|
+
context "array_has" do
|
7
|
+
let!(:match) { TextArrayRecord.create! :texts => %w{a b} }
|
8
|
+
let!(:missing_element) { TextArrayRecord.create! :texts => %w{a} }
|
9
|
+
|
10
|
+
def self.shared_examples
|
11
|
+
it { should include(match) }
|
12
|
+
it { should_not include(missing_element) }
|
13
|
+
it { should_not include(empty) }
|
14
|
+
end
|
15
|
+
|
16
|
+
context "with one element" do
|
17
|
+
subject { TextArrayRecord.array_has(:texts, "b").all }
|
18
|
+
shared_examples
|
19
|
+
end
|
20
|
+
|
21
|
+
context "with array of elements" do
|
22
|
+
subject { TextArrayRecord.array_has(:texts, ["a", "b"]).all }
|
23
|
+
shared_examples
|
24
|
+
end
|
25
|
+
|
26
|
+
context "with multiple elements" do
|
27
|
+
subject { TextArrayRecord.array_has(:texts, "a", "b").all }
|
28
|
+
shared_examples
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "array_has_any" do
|
33
|
+
let!(:match) { TextArrayRecord.create! :texts => %w{a b} }
|
34
|
+
let!(:missing_element) { TextArrayRecord.create! :texts => %w{a} }
|
35
|
+
|
36
|
+
def self.shared_examples
|
37
|
+
it { should include(match) }
|
38
|
+
it { should_not include(missing_element) }
|
39
|
+
it { should_not include(empty) }
|
40
|
+
end
|
41
|
+
|
42
|
+
context "with one element" do
|
43
|
+
subject { TextArrayRecord.array_has_any(:texts, "b").all }
|
44
|
+
shared_examples
|
45
|
+
end
|
46
|
+
|
47
|
+
context "with array of elements" do
|
48
|
+
subject { TextArrayRecord.array_has_any(:texts, ["b", "c"]).all }
|
49
|
+
shared_examples
|
50
|
+
end
|
51
|
+
|
52
|
+
context "with multiple elements" do
|
53
|
+
subject { TextArrayRecord.array_has_any(:texts, "b", "c").all }
|
54
|
+
shared_examples
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Surus::Array::TextSerializer do
|
4
|
+
round_trip_examples = [
|
5
|
+
[nil, "nil"],
|
6
|
+
[[], "empty array"],
|
7
|
+
[["foo"], "single element"],
|
8
|
+
[["foo", "bar"], "multiple elements"],
|
9
|
+
[["foo", "bar", "bar", "bar"], "duplicated elements"],
|
10
|
+
[["foo bar", nil], "an element is nil"],
|
11
|
+
[["foo bar", 'NULL'], "an element is the string 'NULL'"],
|
12
|
+
[["foo bar", "baz"], "an element has a space"],
|
13
|
+
[["foo,bar", "baz"], "an element has a comma (,)"],
|
14
|
+
[["foo'bar", "baz"], "an element has a single quote (')"],
|
15
|
+
[['foo"bar', "baz"], "an element has a double quote (\")"],
|
16
|
+
[['foo\\bar', "baz"], "an element has a backslash (\\)"],
|
17
|
+
[['foo{}bar', "{baz}"], "an element has a braces ({})"],
|
18
|
+
[[%q~foo \\ / " ; ' ( ) {}bar \\'~, "bar"], "an element many special characters"],
|
19
|
+
[("aaa".."zzz").to_a, "huge array"]
|
20
|
+
]
|
21
|
+
|
22
|
+
round_trip_examples.each do |value, description|
|
23
|
+
it "round trips when #{description}" do
|
24
|
+
r = TextArrayRecord.create! :texts => value
|
25
|
+
r.reload
|
26
|
+
r.texts.should == value
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/spec/database.yml
CHANGED
data/spec/database_structure.sql
CHANGED
@@ -7,3 +7,38 @@ CREATE TABLE hstore_records(
|
|
7
7
|
properties hstore
|
8
8
|
);
|
9
9
|
|
10
|
+
|
11
|
+
|
12
|
+
DROP TABLE IF EXISTS text_array_records;
|
13
|
+
|
14
|
+
CREATE TABLE text_array_records(
|
15
|
+
id serial PRIMARY KEY,
|
16
|
+
texts text[]
|
17
|
+
);
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
DROP TABLE IF EXISTS integer_array_records;
|
22
|
+
|
23
|
+
CREATE TABLE integer_array_records(
|
24
|
+
id serial PRIMARY KEY,
|
25
|
+
integers integer[]
|
26
|
+
);
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
DROP TABLE IF EXISTS float_array_records;
|
31
|
+
|
32
|
+
CREATE TABLE float_array_records(
|
33
|
+
id serial PRIMARY KEY,
|
34
|
+
floats float[]
|
35
|
+
);
|
36
|
+
|
37
|
+
|
38
|
+
|
39
|
+
DROP TABLE IF EXISTS float_array_records;
|
40
|
+
|
41
|
+
CREATE TABLE decimal_array_records(
|
42
|
+
id serial PRIMARY KEY,
|
43
|
+
decimals decimal[]
|
44
|
+
);
|
data/spec/hstore/scope_spec.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe Hstore::Serializer do
|
3
|
+
describe Surus::Hstore::Serializer do
|
4
4
|
round_trip_examples = [
|
5
5
|
[nil, "nil"],
|
6
6
|
[{}, "empty hash"],
|
@@ -44,6 +44,8 @@ describe Hstore::Serializer do
|
|
44
44
|
[BigDecimal("0"), "BigDecimal 0"],
|
45
45
|
[BigDecimal("1"), "positive BigDecimal"],
|
46
46
|
[BigDecimal("-1"), "negative BigDecimal"],
|
47
|
+
[true, "true"],
|
48
|
+
[false, "false"],
|
47
49
|
[Date.today, "date"]
|
48
50
|
].each do |value, description|
|
49
51
|
round_trip_examples << [{"foo" => value}, "value is #{description}"]
|
data/spec/spec_helper.rb
CHANGED
@@ -7,8 +7,25 @@ database_config = YAML.load_file(File.expand_path("../database.yml", __FILE__))
|
|
7
7
|
ActiveRecord::Base.establish_connection database_config["test"]
|
8
8
|
|
9
9
|
|
10
|
+
|
10
11
|
class HstoreRecord < ActiveRecord::Base
|
11
|
-
serialize :properties, Hstore::Serializer.new
|
12
|
+
serialize :properties, Surus::Hstore::Serializer.new
|
13
|
+
end
|
14
|
+
|
15
|
+
class TextArrayRecord < ActiveRecord::Base
|
16
|
+
serialize :texts, Surus::Array::TextSerializer.new
|
17
|
+
end
|
18
|
+
|
19
|
+
class IntegerArrayRecord < ActiveRecord::Base
|
20
|
+
serialize :integers, Surus::Array::IntegerSerializer.new
|
21
|
+
end
|
22
|
+
|
23
|
+
class FloatArrayRecord < ActiveRecord::Base
|
24
|
+
serialize :floats, Surus::Array::FloatSerializer.new
|
25
|
+
end
|
26
|
+
|
27
|
+
class DecimalArrayRecord < ActiveRecord::Base
|
28
|
+
serialize :decimals, Surus::Array::DecimalSerializer.new
|
12
29
|
end
|
13
30
|
|
14
31
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: surus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-02-
|
12
|
+
date: 2012-02-03 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: pg
|
16
|
-
requirement: &
|
16
|
+
requirement: &17079080 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *17079080
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: activerecord
|
27
|
-
requirement: &
|
27
|
+
requirement: &17091280 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 3.1.0
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *17091280
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rspec
|
38
|
-
requirement: &
|
38
|
+
requirement: &17089120 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ~>
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: 2.8.0
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *17089120
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: guard
|
49
|
-
requirement: &
|
49
|
+
requirement: &17086120 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: 0.10.0
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *17086120
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: guard-rspec
|
60
|
-
requirement: &
|
60
|
+
requirement: &17102600 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ! '>='
|
@@ -65,7 +65,7 @@ dependencies:
|
|
65
65
|
version: 0.6.0
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *17102600
|
69
69
|
description: PostgreSQL extensions for ActiveRecord
|
70
70
|
email:
|
71
71
|
- jack@jackchristensen.com
|
@@ -75,15 +75,26 @@ extra_rdoc_files: []
|
|
75
75
|
files:
|
76
76
|
- .gitignore
|
77
77
|
- .rspec
|
78
|
+
- CHANGELOG.md
|
78
79
|
- Gemfile
|
79
80
|
- Guardfile
|
80
81
|
- LICENSE
|
81
82
|
- README.md
|
82
83
|
- Rakefile
|
83
84
|
- lib/surus.rb
|
85
|
+
- lib/surus/array/decimal_serializer.rb
|
86
|
+
- lib/surus/array/float_serializer.rb
|
87
|
+
- lib/surus/array/integer_serializer.rb
|
88
|
+
- lib/surus/array/scope.rb
|
89
|
+
- lib/surus/array/text_serializer.rb
|
84
90
|
- lib/surus/hstore/scope.rb
|
85
91
|
- lib/surus/hstore/serializer.rb
|
86
92
|
- lib/surus/version.rb
|
93
|
+
- spec/array/decimal_serializer_spec.rb
|
94
|
+
- spec/array/float_serializer_spec.rb
|
95
|
+
- spec/array/integer_serializer_spec.rb
|
96
|
+
- spec/array/scope_spec.rb
|
97
|
+
- spec/array/text_serializer_spec.rb
|
87
98
|
- spec/database.yml
|
88
99
|
- spec/database_structure.sql
|
89
100
|
- spec/hstore/scope_spec.rb
|