hstore_accessor 0.6.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,10 +1,16 @@
1
+ require "active_support"
2
+ require "active_record"
1
3
  require "hstore_accessor/version"
2
- require "hstore_accessor/type_helpers"
4
+
5
+ if ::ActiveRecord::VERSION::STRING.to_f >= 4.2
6
+ require "hstore_accessor/active_record_4.2/type_helpers"
7
+ else
8
+ require "hstore_accessor/active_record_<_4.2/type_helpers"
9
+ require "hstore_accessor/active_record_<_4.2/time_helper"
10
+ end
11
+
3
12
  require "hstore_accessor/serialization"
4
13
  require "hstore_accessor/macro"
5
- require "hstore_accessor/time_helper"
6
- require "active_support"
7
- require "active_record"
8
14
  require "bigdecimal"
9
15
 
10
16
  module HstoreAccessor
@@ -0,0 +1,34 @@
1
+ module HstoreAccessor
2
+ module TypeHelpers
3
+ TYPES = {
4
+ boolean: ActiveRecord::Type::Boolean,
5
+ date: ActiveRecord::Type::Date,
6
+ datetime: ActiveRecord::Type::DateTime,
7
+ decimal: ActiveRecord::Type::Decimal,
8
+ float: ActiveRecord::Type::Float,
9
+ integer: ActiveRecord::Type::Integer,
10
+ string: ActiveRecord::Type::String
11
+ }
12
+
13
+ TYPES.default = ActiveRecord::Type::Value
14
+
15
+ class << self
16
+ def column_type_for(attribute, data_type)
17
+ ActiveRecord::ConnectionAdapters::Column.new(attribute.to_s, nil, TYPES[data_type].new)
18
+ end
19
+
20
+ def cast(type, value)
21
+ return nil if value.nil?
22
+
23
+ case type
24
+ when :string, :hash, :array, :decimal
25
+ value
26
+ when :integer, :float, :datetime, :date, :boolean
27
+ TYPES[type].new.type_cast_from_user(value)
28
+ else value
29
+ # Nothing.
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,6 +1,5 @@
1
1
  module HstoreAccessor
2
2
  module TimeHelper
3
-
4
3
  # There is a bug in ActiveRecord::ConnectionAdapters::Column#string_to_time
5
4
  # which drops the timezone. This has been fixed, but not released.
6
5
  # This method includes the fix. See: https://github.com/rails/rails/pull/12290
@@ -11,10 +10,10 @@ module HstoreAccessor
11
10
 
12
11
  time_hash = Date._parse(string)
13
12
  time_hash[:sec_fraction] = ActiveRecord::ConnectionAdapters::Column.send(:microseconds, time_hash)
14
- (year, mon, mday, hour, min, sec, microsec, offset) = *time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)
13
+ year, mon, mday, hour, min, sec, microsec, offset = time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)
15
14
 
16
15
  # Treat 0000-00-00 00:00:00 as nil.
17
- return nil if year.nil? || (year == 0 && mon == 0 && mday == 0)
16
+ return nil if year.nil? || [year, mon, mday].all?(&:zero?)
18
17
 
19
18
  if offset
20
19
  time = Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
@@ -26,6 +25,5 @@ module HstoreAccessor
26
25
  Time.public_send(ActiveRecord::Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
27
26
  end
28
27
  end
29
-
30
28
  end
31
29
  end
@@ -0,0 +1,42 @@
1
+ module HstoreAccessor
2
+ module TypeHelpers
3
+ TYPES = {
4
+ string: "char",
5
+ datetime: "datetime",
6
+ date: "date",
7
+ float: "float",
8
+ boolean: "boolean",
9
+ decimal: "decimal",
10
+ integer: "int"
11
+ }
12
+
13
+ class << self
14
+ def column_type_for(attribute, data_type)
15
+ ActiveRecord::ConnectionAdapters::Column.new(attribute.to_s, nil, TYPES[data_type])
16
+ end
17
+
18
+ def cast(type, value)
19
+ return nil if value.nil?
20
+
21
+ column_class = ActiveRecord::ConnectionAdapters::Column
22
+
23
+ case type
24
+ when :string, :hash, :array, :decimal
25
+ value
26
+ when :integer
27
+ column_class.value_to_integer(value)
28
+ when :float
29
+ value.to_f
30
+ when :datetime
31
+ TimeHelper.string_to_time(value)
32
+ when :date
33
+ column_class.value_to_date(value)
34
+ when :boolean
35
+ column_class.value_to_boolean(value)
36
+ else value
37
+ # Nothing.
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,18 +1,52 @@
1
1
  module HstoreAccessor
2
2
  module Macro
3
-
4
3
  module ClassMethods
5
-
6
4
  def hstore_accessor(hstore_attribute, fields)
7
- define_method("hstore_metadata_for_#{hstore_attribute}") do
8
- fields
5
+ @@hstore_keys_and_types ||= {}
6
+
7
+ "hstore_metadata_for_#{hstore_attribute}".tap do |method_name|
8
+ singleton_class.send(:define_method, method_name) do
9
+ fields
10
+ end
11
+
12
+ delegate method_name, to: :class
9
13
  end
10
14
 
11
15
  field_methods = Module.new
12
- fields.each do |key, type|
13
16
 
17
+ if ActiveRecord::VERSION::STRING.to_f >= 4.2
18
+ singleton_class.send(:define_method, :type_for_attribute) do |attribute|
19
+ data_type = @@hstore_keys_and_types[attribute]
20
+ if data_type
21
+ TypeHelpers::TYPES[data_type].new
22
+ else
23
+ super(attribute)
24
+ end
25
+ end
26
+
27
+ singleton_class.send(:define_method, :column_for_attribute) do |attribute|
28
+ data_type = @@hstore_keys_and_types[attribute.to_s]
29
+ if data_type
30
+ TypeHelpers.column_type_for(attribute.to_s, data_type)
31
+ else
32
+ super(attribute)
33
+ end
34
+ end
35
+ else
36
+ field_methods.send(:define_method, :column_for_attribute) do |attribute|
37
+ data_type = @@hstore_keys_and_types[attribute.to_s]
38
+ if data_type
39
+ TypeHelpers.column_type_for(attribute.to_s, data_type)
40
+ else
41
+ super(attribute)
42
+ end
43
+ end
44
+ end
45
+
46
+ fields.each do |key, type|
14
47
  data_type = type
15
48
  store_key = key
49
+
16
50
  if type.is_a?(Hash)
17
51
  type = type.with_indifferent_access
18
52
  data_type = type[:data_type]
@@ -23,71 +57,101 @@ module HstoreAccessor
23
57
 
24
58
  raise Serialization::InvalidDataTypeError unless Serialization::VALID_TYPES.include?(data_type)
25
59
 
26
- field_methods.send(:define_method, "#{key}=") do |value|
27
- casted_value = TypeHelpers.cast(data_type, value)
28
- serialized_value = serialize(data_type, casted_value)
29
- unless send(key) == casted_value
30
- send(:attribute_will_change!, key)
31
- send("#{hstore_attribute}_will_change!")
60
+ @@hstore_keys_and_types[key.to_s] = data_type
61
+
62
+ field_methods.instance_eval do
63
+ define_method("#{key}=") do |value|
64
+ casted_value = TypeHelpers.cast(data_type, value)
65
+ serialized_value = Serialization.serialize(data_type, casted_value)
66
+
67
+ unless send(key) == casted_value
68
+ send("#{hstore_attribute}_will_change!")
69
+ end
70
+
71
+ send("#{hstore_attribute}=", (send(hstore_attribute) || {}).merge(store_key.to_s => serialized_value))
32
72
  end
33
- send("#{hstore_attribute}=", (send(hstore_attribute) || {}).merge(store_key.to_s => serialized_value))
34
- end
35
73
 
36
- field_methods.send(:define_method, key) do
37
- value = send(hstore_attribute) && send(hstore_attribute).with_indifferent_access[store_key.to_s]
38
- deserialize(data_type, value)
39
- end
74
+ define_method(key) do
75
+ value = send(hstore_attribute) && send(hstore_attribute).with_indifferent_access[store_key.to_s]
76
+ Serialization.deserialize(data_type, value)
77
+ end
40
78
 
41
- field_methods.send(:define_method, "#{key}?") do
42
- send("#{key}").present?
43
- end
79
+ define_method("#{key}?") do
80
+ send(key).present?
81
+ end
44
82
 
45
- field_methods.send(:define_method, "#{key}_changed?") do
46
- send(:attribute_changed?, key)
47
- end
83
+ define_method("#{key}_changed?") do
84
+ send("#{key}_change").present?
85
+ end
48
86
 
49
- field_methods.send(:define_method, "#{key}_was") do
50
- send(:attribute_was, key)
51
- end
87
+ define_method("#{key}_was") do
88
+ (send(:attribute_was, hstore_attribute.to_s) || {})[key.to_s]
89
+ end
90
+
91
+ define_method("#{key}_change") do
92
+ hstore_changes = send("#{hstore_attribute}_change")
93
+ return if hstore_changes.nil?
94
+ attribute_changes = hstore_changes.map { |change| change.try(:[], store_key.to_s) }
95
+ attribute_changes.compact.present? ? attribute_changes : nil
96
+ end
52
97
 
53
- field_methods.send(:define_method, "#{key}_change") do
54
- send(:attribute_change, key)
98
+ define_method("restore_#{key}!") do
99
+ old_hstore = send("#{hstore_attribute}_change").try(:first) || {}
100
+ send("#{key}=", old_hstore[key.to_s])
101
+ end
102
+
103
+ define_method("reset_#{key}!") do
104
+ if ActiveRecord::VERSION::STRING.to_f >= 4.2
105
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
106
+ `#reset_#{key}!` is deprecated and will be removed on Rails 5.
107
+ Please use `#restore_#{key}!` instead.
108
+ MSG
109
+ end
110
+ send("restore_#{key}!")
111
+ end
112
+
113
+ define_method("#{key}_will_change!") do
114
+ send("#{hstore_attribute}_will_change!")
115
+ end
55
116
  end
56
117
 
57
118
  query_field = "#{hstore_attribute} -> '#{store_key}'"
119
+ eq_query_field = "#{hstore_attribute} @> hstore('#{store_key}', ?)"
58
120
 
59
121
  case data_type
60
122
  when :string
61
- send(:scope, "with_#{key}", -> value { where("#{query_field} = ?", value.to_s) })
62
- when :integer, :float, :decimal
63
- send(:scope, "#{key}_lt", -> value { where("(#{query_field})::#{data_type} < ?", value.to_s) })
123
+ send(:scope, "with_#{key}", -> value { where(eq_query_field, value.to_s) })
124
+ when :integer
125
+ send(:scope, "#{key}_lt", -> value { where("(#{query_field})::#{data_type} < ?", value.to_s) })
126
+ send(:scope, "#{key}_lte", -> value { where("(#{query_field})::#{data_type} <= ?", value.to_s) })
127
+ send(:scope, "#{key}_eq", -> value { where(eq_query_field, value.to_s) })
128
+ send(:scope, "#{key}_gte", -> value { where("(#{query_field})::#{data_type} >= ?", value.to_s) })
129
+ send(:scope, "#{key}_gt", -> value { where("(#{query_field})::#{data_type} > ?", value.to_s) })
130
+ when :float, :decimal
131
+ send(:scope, "#{key}_lt", -> value { where("(#{query_field})::#{data_type} < ?", value.to_s) })
64
132
  send(:scope, "#{key}_lte", -> value { where("(#{query_field})::#{data_type} <= ?", value.to_s) })
65
- send(:scope, "#{key}_eq", -> value { where("(#{query_field})::#{data_type} = ?", value.to_s) })
133
+ send(:scope, "#{key}_eq", -> value { where("(#{query_field})::#{data_type} = ?", value.to_s) })
66
134
  send(:scope, "#{key}_gte", -> value { where("(#{query_field})::#{data_type} >= ?", value.to_s) })
67
- send(:scope, "#{key}_gt", -> value { where("(#{query_field})::#{data_type} > ?", value.to_s) })
68
- when :time
135
+ send(:scope, "#{key}_gt", -> value { where("(#{query_field})::#{data_type} > ?", value.to_s) })
136
+ when :datetime
69
137
  send(:scope, "#{key}_before", -> value { where("(#{query_field})::integer < ?", value.to_i) })
70
- send(:scope, "#{key}_eq", -> value { where("(#{query_field})::integer = ?", value.to_i) })
71
- send(:scope, "#{key}_after", -> value { where("(#{query_field})::integer > ?", value.to_i) })
138
+ send(:scope, "#{key}_eq", -> value { where(eq_query_field, value.to_i.to_s) })
139
+ send(:scope, "#{key}_after", -> value { where("(#{query_field})::integer > ?", value.to_i) })
72
140
  when :date
73
141
  send(:scope, "#{key}_before", -> value { where("#{query_field} < ?", value.to_s) })
74
- send(:scope, "#{key}_eq", -> value { where("#{query_field} = ?", value.to_s) })
75
- send(:scope, "#{key}_after", -> value { where("#{query_field} > ?", value.to_s) })
142
+ send(:scope, "#{key}_eq", -> value { where(eq_query_field, value.to_s) })
143
+ send(:scope, "#{key}_after", -> value { where("#{query_field} > ?", value.to_s) })
76
144
  when :boolean
77
- send(:scope, "is_#{key}", -> { where("#{query_field} = 'true'") })
78
- send(:scope, "not_#{key}", -> { where("#{query_field} = 'false'") })
145
+ send(:scope, "is_#{key}", -> { where(eq_query_field, "true") })
146
+ send(:scope, "not_#{key}", -> { where(eq_query_field, "false") })
79
147
  when :array
80
- send(:scope, "#{key}_eq", -> value { where("#{query_field} = ?", value.join(Serialization::SEPARATOR)) })
81
- send(:scope, "#{key}_contains", -> value do
82
- where("string_to_array(#{query_field}, '#{Serialization::SEPARATOR}') @> string_to_array(?, '#{Serialization::SEPARATOR}')", Array[value].flatten.join(Serialization::SEPARATOR))
83
- end)
148
+ send(:scope, "#{key}_eq", -> value { where("#{query_field} = ?", value.join(Serialization::SEPARATOR)) })
149
+ send(:scope, "#{key}_contains", -> value { where("string_to_array(#{query_field}, '#{Serialization::SEPARATOR}') @> string_to_array(?, '#{Serialization::SEPARATOR}')", Array[value].flatten.join(Serialization::SEPARATOR)) })
84
150
  end
85
151
  end
86
152
 
87
153
  include field_methods
88
154
  end
89
-
90
155
  end
91
-
92
156
  end
93
157
  end
@@ -1,45 +1,57 @@
1
1
  module HstoreAccessor
2
2
  module Serialization
3
-
4
3
  InvalidDataTypeError = Class.new(StandardError)
5
4
 
6
- VALID_TYPES = [:string, :integer, :float, :time, :boolean, :array, :hash, :date, :decimal]
7
-
8
- SEPARATOR = "||;||"
5
+ VALID_TYPES = [
6
+ :array,
7
+ :boolean,
8
+ :date,
9
+ :datetime,
10
+ :decimal,
11
+ :float,
12
+ :hash,
13
+ :integer,
14
+ :string
15
+ ]
9
16
 
10
17
  DEFAULT_SERIALIZER = ->(value) { value.to_s }
11
18
  DEFAULT_DESERIALIZER = DEFAULT_SERIALIZER
12
19
 
20
+ SEPARATOR = "||;||"
21
+
13
22
  SERIALIZERS = {
14
- array: -> value { (value && value.join(SEPARATOR)) || nil },
15
- hash: -> value { (value && value.to_json) || nil },
16
- time: -> value { value.to_i },
17
- boolean: -> value { (value.to_s == "true").to_s },
18
- date: -> value { (value && value.to_s) || nil }
23
+ array: -> value { (value && value.join(SEPARATOR)) || nil },
24
+ boolean: -> value { (value.to_s == "true").to_s },
25
+ date: -> value { value && value.to_s },
26
+ hash: -> value { (value && value.to_json) || nil },
27
+ datetime: -> value { value && value.to_i }
19
28
  }
29
+ SERIALIZERS.default = DEFAULT_SERIALIZER
20
30
 
21
31
  DESERIALIZERS = {
22
- array: -> value { (value && value.split(SEPARATOR)) || nil },
23
- hash: -> value { (value && JSON.parse(value)) || nil },
24
- integer: -> value { value.to_i },
25
- float: -> value { value.to_f },
26
- time: -> value { Time.at(value.to_i) },
27
- boolean: -> value { TypeHelpers.cast(:boolean, value) },
28
- date: -> value { (value && Date.parse(value)) || nil },
29
- decimal: -> value { BigDecimal.new(value) }
32
+ array: -> value { (value && value.split(SEPARATOR)) || nil },
33
+ boolean: -> value { TypeHelpers.cast(:boolean, value) },
34
+ date: -> value { value && Date.parse(value) },
35
+ decimal: -> value { value && BigDecimal.new(value) },
36
+ float: -> value { value && value.to_f },
37
+ hash: -> value { (value && JSON.parse(value)) || nil },
38
+ integer: -> value { value && value.to_i },
39
+ datetime: -> value { value && Time.at(value.to_i).in_time_zone }
30
40
  }
41
+ DESERIALIZERS.default = DEFAULT_DESERIALIZER
31
42
 
32
- def serialize(type, value, serializer=nil)
33
- return nil if value.nil?
34
- serializer ||= (SERIALIZERS[type] || DEFAULT_SERIALIZER)
35
- serializer.call(value)
36
- end
43
+ class << self
44
+ def serialize(type, value, serializer=SERIALIZERS[type])
45
+ return nil if value.nil?
37
46
 
38
- def deserialize(type, value, deserializer=nil)
39
- return nil if value.nil?
40
- deserializer ||= (DESERIALIZERS[type] || DEFAULT_DESERIALIZER)
41
- deserializer.call(value)
42
- end
47
+ serializer.call(value)
48
+ end
43
49
 
50
+ def deserialize(type, value, deserializer=DESERIALIZERS[type])
51
+ return nil if value.nil?
52
+
53
+ deserializer.call(value)
54
+ end
55
+ end
44
56
  end
45
57
  end
@@ -1,3 +1,3 @@
1
1
  module HstoreAccessor
2
- VERSION = "0.6.1"
2
+ VERSION = "0.9.0"
3
3
  end
@@ -7,30 +7,34 @@ FIELDS = {
7
7
  published: { data_type: :boolean, store_key: "p" },
8
8
  weight: { data_type: :float, store_key: "w" },
9
9
  popular: :boolean,
10
- build_timestamp: :time,
10
+ build_timestamp: :datetime,
11
11
  tags: :array,
12
12
  reviews: :hash,
13
13
  released_at: :date,
14
14
  miles: :decimal
15
15
  }
16
16
 
17
+ DATA_FIELDS = {
18
+ color_data: :string
19
+ }
20
+
17
21
  class Product < ActiveRecord::Base
18
22
  hstore_accessor :options, FIELDS
23
+ hstore_accessor :data, DATA_FIELDS
19
24
  end
20
25
 
21
- describe HstoreAccessor do
26
+ class SuperProduct < Product
27
+ end
22
28
 
29
+ describe HstoreAccessor do
23
30
  context "macro" do
24
-
25
31
  let(:product) { Product.new }
26
32
 
27
33
  FIELDS.keys.each do |field|
28
34
  it "creates a getter for the hstore field: #{field}" do
29
35
  expect(product).to respond_to(field)
30
36
  end
31
- end
32
37
 
33
- FIELDS.keys.each do |field|
34
38
  it "creates a setter for the hstore field: #{field}=" do
35
39
  expect(product).to respond_to(:"#{field}=")
36
40
  end
@@ -52,43 +56,42 @@ describe HstoreAccessor do
52
56
  expect(product.options["w"]).to eq "38.5"
53
57
  expect(product.weight).to eq 38.5
54
58
  end
55
-
56
59
  end
57
60
 
58
- context "#__hstore_metadata_for_*" do
59
-
60
- let(:product) { Product.new }
61
+ context "#hstore_metadata_for_*" do
62
+ let(:product) { Product }
61
63
 
62
64
  it "returns the metadata hash for the specified field" do
63
65
  expect(product.hstore_metadata_for_options).to eq FIELDS
66
+ expect(product.hstore_metadata_for_data).to eq DATA_FIELDS
64
67
  end
65
68
 
69
+ context "instance method" do
70
+ subject { Product.new }
71
+ it { is_expected.to delegate_method(:hstore_metadata_for_options).to(:class) }
72
+ it { is_expected.to delegate_method(:hstore_metadata_for_data).to(:class) }
73
+ end
66
74
  end
67
75
 
68
76
  context "nil values" do
69
-
70
77
  let!(:timestamp) { Time.now }
71
78
  let!(:datestamp) { Date.today }
72
- let!(:product) { Product.new }
73
- let!(:product_a) { Product.create(color: "green", price: 10, weight: 10.1, tags: ["tag1", "tag2", "tag3"], popular: true, build_timestamp: (timestamp - 10.days), released_at: (datestamp - 8.days), miles: BigDecimal.new('9.133790001')) }
79
+ let(:product) { Product.new }
80
+ let(:persisted_product) { Product.create!(color: "green", price: 10, weight: 10.1, tags: %w(tag1 tag2 tag3), popular: true, build_timestamp: (timestamp - 10.days), released_at: (datestamp - 8.days), miles: BigDecimal.new("9.133790001")) }
74
81
 
75
82
  FIELDS.keys.each do |field|
76
83
  it "responds with nil when #{field} is not set" do
77
84
  expect(product.send(field)).to be_nil
78
85
  end
79
- end
80
86
 
81
- FIELDS.keys.each do |field|
82
87
  it "responds with nil when #{field} is set back to nil after being set initially" do
83
- product_a.send("#{field}=", nil)
84
- expect(product_a.send(field)).to be_nil
88
+ persisted_product.send("#{field}=", nil)
89
+ expect(persisted_product.send(field)).to be_nil
85
90
  end
86
91
  end
87
-
88
92
  end
89
93
 
90
94
  describe "predicate methods" do
91
-
92
95
  let!(:product) { Product.new }
93
96
 
94
97
  it "exist for each field" do
@@ -105,7 +108,6 @@ describe HstoreAccessor do
105
108
  end
106
109
 
107
110
  context "boolean fields" do
108
-
109
111
  it "return the state for true boolean fields" do
110
112
  product.popular = true
111
113
  product.save
@@ -140,27 +142,22 @@ describe HstoreAccessor do
140
142
  expect(product.popular?).to be false
141
143
  end
142
144
  end
143
-
144
145
  end
145
146
 
146
147
  describe "scopes" do
147
-
148
148
  let!(:timestamp) { Time.now }
149
149
  let!(:datestamp) { Date.today }
150
- let!(:product_a) { Product.create(color: "green", price: 10, weight: 10.1, tags: ["tag1", "tag2", "tag3"], popular: true, build_timestamp: (timestamp - 10.days), released_at: (datestamp - 8.days), miles: BigDecimal.new('10.113379001')) }
151
- let!(:product_b) { Product.create(color: "orange", price: 20, weight: 20.2, tags: ["tag2", "tag3", "tag4"], popular: false, build_timestamp: (timestamp - 5.days), released_at: (datestamp - 4.days), miles: BigDecimal.new('20.213379001')) }
152
- let!(:product_c) { Product.create(color: "blue", price: 30, weight: 30.3, tags: ["tag3", "tag4", "tag5"], popular: true, build_timestamp: timestamp, released_at: datestamp, miles: BigDecimal.new('30.313379001')) }
150
+ let!(:product_a) { Product.create(color: "green", price: 10, weight: 10.1, tags: %w(tag1 tag2 tag3), popular: true, build_timestamp: (timestamp - 10.days), released_at: (datestamp - 8.days), miles: BigDecimal.new("10.113379001")) }
151
+ let!(:product_b) { Product.create(color: "orange", price: 20, weight: 20.2, tags: %w(tag2 tag3 tag4), popular: false, build_timestamp: (timestamp - 5.days), released_at: (datestamp - 4.days), miles: BigDecimal.new("20.213379001")) }
152
+ let!(:product_c) { Product.create(color: "blue", price: 30, weight: 30.3, tags: %w(tag3 tag4 tag5), popular: true, build_timestamp: timestamp, released_at: datestamp, miles: BigDecimal.new("30.313379001")) }
153
153
 
154
154
  context "for string fields support" do
155
-
156
155
  it "equality" do
157
156
  expect(Product.with_color("orange").to_a).to eq [product_b]
158
157
  end
159
-
160
158
  end
161
159
 
162
160
  context "for integer fields support" do
163
-
164
161
  it "less than" do
165
162
  expect(Product.price_lt(20).to_a).to eq [product_a]
166
163
  end
@@ -180,11 +177,9 @@ describe HstoreAccessor do
180
177
  it "greater than" do
181
178
  expect(Product.price_gt(20).to_a).to eq [product_c]
182
179
  end
183
-
184
180
  end
185
181
 
186
182
  context "for float fields support" do
187
-
188
183
  it "less than" do
189
184
  expect(Product.weight_lt(20.0).to_a).to eq [product_a]
190
185
  end
@@ -204,50 +199,44 @@ describe HstoreAccessor do
204
199
  it "greater than" do
205
200
  expect(Product.weight_gt(20.5).to_a).to eq [product_c]
206
201
  end
207
-
208
202
  end
209
203
 
210
204
  context "for decimal fields support" do
211
-
212
205
  it "less than" do
213
- expect(Product.miles_lt(BigDecimal.new('10.55555')).to_a).to eq [product_a]
206
+ expect(Product.miles_lt(BigDecimal.new("10.55555")).to_a).to eq [product_a]
214
207
  end
215
208
 
216
209
  it "less than or equal" do
217
- expect(Product.miles_lte(BigDecimal.new('20.213379001')).to_a).to eq [product_a, product_b]
210
+ expect(Product.miles_lte(BigDecimal.new("20.213379001")).to_a).to eq [product_a, product_b]
218
211
  end
219
212
 
220
213
  it "equality" do
221
- expect(Product.miles_eq(BigDecimal.new('10.113379001')).to_a).to eq [product_a]
214
+ expect(Product.miles_eq(BigDecimal.new("10.113379001")).to_a).to eq [product_a]
222
215
  end
223
216
 
224
217
  it "greater than or equal" do
225
- expect(Product.miles_gte(BigDecimal.new('20.213379001')).to_a).to eq [product_b, product_c]
218
+ expect(Product.miles_gte(BigDecimal.new("20.213379001")).to_a).to eq [product_b, product_c]
226
219
  end
227
220
 
228
221
  it "greater than" do
229
- expect(Product.miles_gt(BigDecimal.new('20.555555')).to_a).to eq [product_c]
222
+ expect(Product.miles_gt(BigDecimal.new("20.555555")).to_a).to eq [product_c]
230
223
  end
231
-
232
224
  end
233
225
 
234
226
  context "for array fields support" do
235
-
236
227
  it "equality" do
237
- expect(Product.tags_eq(["tag1", "tag2", "tag3"]).to_a).to eq [product_a]
228
+ expect(Product.tags_eq(%w(tag1 tag2 tag3)).to_a).to eq [product_a]
238
229
  end
239
230
 
240
231
  it "contains" do
241
232
  expect(Product.tags_contains("tag2").to_a).to eq [product_a, product_b]
242
- expect(Product.tags_contains(["tag2", "tag3"]).to_a).to eq [product_a, product_b]
243
- expect(Product.tags_contains(["tag1", "tag2", "tag3"]).to_a).to eq [product_a]
244
- expect(Product.tags_contains(["tag1", "tag2", "tag3", "tag4"]).to_a).to eq []
233
+ expect(Product.tags_contains(%w(tag2 tag3)).to_a).to eq [product_a, product_b]
234
+ expect(Product.tags_contains(%w(tag1 tag2 tag3)).to_a).to eq [product_a]
235
+ expect(Product.tags_contains(%w(tag1 tag2 tag3 tag4)).to_a).to eq []
245
236
  end
246
-
247
237
  end
248
238
 
249
239
  context "for time fields support" do
250
-
251
240
  it "before" do
252
241
  expect(Product.build_timestamp_before(timestamp)).to eq [product_a, product_b]
253
242
  end
@@ -259,11 +248,9 @@ describe HstoreAccessor do
259
248
  it "after" do
260
249
  expect(Product.build_timestamp_after(timestamp - 6.days)).to eq [product_b, product_c]
261
250
  end
262
-
263
251
  end
264
252
 
265
253
  context "for date fields support" do
266
-
267
254
  it "before" do
268
255
  expect(Product.released_at_before(datestamp)).to eq [product_a, product_b]
269
256
  end
@@ -275,11 +262,9 @@ describe HstoreAccessor do
275
262
  it "after" do
276
263
  expect(Product.released_at_after(datestamp - 6.days)).to eq [product_b, product_c]
277
264
  end
278
-
279
265
  end
280
266
 
281
267
  context "for boolean field support" do
282
-
283
268
  it "true" do
284
269
  expect(Product.is_popular).to eq [product_a, product_c]
285
270
  end
@@ -287,13 +272,81 @@ describe HstoreAccessor do
287
272
  it "false" do
288
273
  expect(Product.not_popular).to eq [product_b]
289
274
  end
275
+ end
276
+ end
277
+
278
+ describe "#type_for_attribute" do
279
+ if ::ActiveRecord::VERSION::STRING.to_f >= 4.2
280
+ subject { SuperProduct }
290
281
 
282
+ def self.it_returns_the_type_for_the_attribute(type, attribute_name, active_record_type)
283
+ context "#{type}" do
284
+ it "returns the type for the column" do
285
+ expect(subject.type_for_attribute(attribute_name.to_s)).to eq(active_record_type.new)
286
+ end
287
+ end
288
+ end
289
+
290
+ it_returns_the_type_for_the_attribute "default behavior", :string_type, ActiveRecord::Type::String
291
+ it_returns_the_type_for_the_attribute :string, :color, ActiveRecord::Type::String
292
+ it_returns_the_type_for_the_attribute :integer, :price, ActiveRecord::Type::Integer
293
+ it_returns_the_type_for_the_attribute :float, :weight, ActiveRecord::Type::Float
294
+ it_returns_the_type_for_the_attribute :datetime, :build_timestamp, ActiveRecord::Type::DateTime
295
+ it_returns_the_type_for_the_attribute :date, :released_at, ActiveRecord::Type::Date
296
+ it_returns_the_type_for_the_attribute :boolean, :published, ActiveRecord::Type::Boolean
297
+ else
298
+ subject { Product }
299
+
300
+ it "is not defined" do
301
+ expect(subject).to_not respond_to(:type_for_attribute)
302
+ end
291
303
  end
304
+ end
305
+
306
+ describe "#column_for_attribute" do
307
+ if ActiveRecord::VERSION::STRING.to_f >= 4.2
308
+
309
+ def self.it_returns_the_properly_typed_column(type, attribute_name, cast_type_class)
310
+ context "#{type}" do
311
+ subject { SuperProduct.column_for_attribute(attribute_name) }
312
+ it "returns a column with a #{type} cast type" do
313
+ expect(subject).to be_a(ActiveRecord::ConnectionAdapters::Column)
314
+ expect(subject.cast_type).to eq(cast_type_class.new)
315
+ end
316
+ end
317
+ end
318
+
319
+ it_returns_the_properly_typed_column :string, :color, ActiveRecord::Type::String
320
+ it_returns_the_properly_typed_column :integer, :price, ActiveRecord::Type::Integer
321
+ it_returns_the_properly_typed_column :boolean, :published, ActiveRecord::Type::Boolean
322
+ it_returns_the_properly_typed_column :float, :weight, ActiveRecord::Type::Float
323
+ it_returns_the_properly_typed_column :datetime, :build_timestamp, ActiveRecord::Type::DateTime
324
+ it_returns_the_properly_typed_column :date, :released_at, ActiveRecord::Type::Date
325
+ it_returns_the_properly_typed_column :decimal, :miles, ActiveRecord::Type::Decimal
326
+ it_returns_the_properly_typed_column :array, :tags, ActiveRecord::Type::Value
327
+ it_returns_the_properly_typed_column :hash, :reviews, ActiveRecord::Type::Value
328
+ else
329
+ def self.it_returns_the_properly_typed_column(hstore_type, attribute_name, active_record_type)
330
+ context "#{hstore_type}" do
331
+ subject { SuperProduct.new.column_for_attribute(attribute_name) }
332
+ it "returns a column with a #{hstore_type} cast type" do
333
+ expect(subject).to be_a(ActiveRecord::ConnectionAdapters::Column)
334
+ expect(subject.type).to eq(active_record_type)
335
+ end
336
+ end
337
+ end
292
338
 
339
+ it_returns_the_properly_typed_column :string, :color, :string
340
+ it_returns_the_properly_typed_column :integer, :price, :integer
341
+ it_returns_the_properly_typed_column :boolean, :published, :boolean
342
+ it_returns_the_properly_typed_column :float, :weight, :float
343
+ it_returns_the_properly_typed_column :time, :build_timestamp, :datetime
344
+ it_returns_the_properly_typed_column :date, :released_at, :date
345
+ it_returns_the_properly_typed_column :decimal, :miles, :decimal
346
+ end
293
347
  end
294
348
 
295
349
  context "when assigning values it" do
296
-
297
350
  let(:product) { Product.new }
298
351
 
299
352
  it "correctly stores string values" do
@@ -335,26 +388,80 @@ describe HstoreAccessor do
335
388
  expect(product.weight).to eq 93.45
336
389
  end
337
390
 
338
- it "correctly stores array values" do
339
- product.tags = ["household", "living room", "kitchen"]
340
- product.save
341
- product.reload
342
- expect(product.tags).to eq ["household", "living room", "kitchen"]
391
+ context "array values" do
392
+ it "correctly stores nothing" do
393
+ product.tags = nil
394
+ product.save
395
+ product.reload
396
+ expect(product.tags).to be_nil
397
+ end
398
+
399
+ it "correctly stores strings" do
400
+ product.tags = ["household", "living room", "kitchen"]
401
+ product.save
402
+ product.reload
403
+ expect(product.tags).to eq ["household", "living room", "kitchen"]
404
+ end
343
405
  end
344
406
 
345
- it "correctly stores hash values" do
346
- product.reviews = { "user_123" => "4 stars", "user_994" => "3 stars" }
347
- product.save
348
- product.reload
349
- expect(product.reviews).to eq({ "user_123" => "4 stars", "user_994" => "3 stars" })
407
+ context "hash values" do
408
+ it "correctly stores nothing" do
409
+ product.reviews = nil
410
+ product.save
411
+ product.reload
412
+ expect(product.reviews).to be_nil
413
+ end
414
+
415
+ it "correctly stores hash values as json" do
416
+ hash = product.reviews = { "user_123" => "4 stars", "user_994" => "3 stars" }
417
+ product.save
418
+ product.reload
419
+ expect(product.reviews).to eq("user_123" => "4 stars", "user_994" => "3 stars")
420
+ expect(product.options["reviews"]).to eq(hash.to_json)
421
+ end
350
422
  end
351
423
 
352
- it "correctly stores time values" do
353
- timestamp = Time.now - 10.days
354
- product.build_timestamp = timestamp
355
- product.save
356
- product.reload
357
- expect(product.build_timestamp.to_i).to eq timestamp.to_i
424
+ context "multipart values" do
425
+ it "stores multipart dates correctly" do
426
+ product.update_attributes!(
427
+ "released_at(1i)" => "2014",
428
+ "released_at(2i)" => "04",
429
+ "released_at(3i)" => "14"
430
+ )
431
+ product.reload
432
+ expect(product.released_at).to eq(Date.new(2014, 4, 14))
433
+ end
434
+ end
435
+
436
+ context "time values" do
437
+ let(:chicago_timezone) { ActiveSupport::TimeZone["America/Chicago"] }
438
+ let(:new_york_timezone) { ActiveSupport::TimeZone["America/New_York"] }
439
+
440
+ it "correctly stores value" do
441
+ timestamp = Time.now - 10.days
442
+ product.build_timestamp = timestamp
443
+ product.save!
444
+ product.reload
445
+ expect(product.build_timestamp.to_i).to eq timestamp.to_i
446
+ end
447
+
448
+ it "stores the value in utc" do
449
+ timestamp = Time.now.in_time_zone(chicago_timezone) - 10.days
450
+ product.build_timestamp = timestamp
451
+ product.save!
452
+ product.reload
453
+ expect(product.options["build_timestamp"].to_i).to eq timestamp.utc.to_i
454
+ end
455
+
456
+ it "returns the time value in the current time zone" do
457
+ timestamp = Time.now.in_time_zone(chicago_timezone) - 10.days
458
+ product.build_timestamp = timestamp
459
+ product.save!
460
+ product.reload
461
+ Time.use_zone(new_york_timezone) do
462
+ expect(product.build_timestamp.utc_offset).to eq new_york_timezone.utc_offset
463
+ end
464
+ end
358
465
  end
359
466
 
360
467
  it "correctly stores date values" do
@@ -367,7 +474,7 @@ describe HstoreAccessor do
367
474
  end
368
475
 
369
476
  it "correctly stores decimal values" do
370
- decimal = BigDecimal.new('9.13370009001')
477
+ decimal = BigDecimal.new("9.13370009001")
371
478
  product.miles = decimal
372
479
  product.save
373
480
  product.reload
@@ -376,9 +483,8 @@ describe HstoreAccessor do
376
483
  end
377
484
 
378
485
  context "correctly stores boolean values" do
379
-
380
486
  it "when string 'true' is passed" do
381
- product.popular = 'true'
487
+ product.popular = "true"
382
488
  product.save
383
489
  product.reload
384
490
  expect(product.popular).to be true
@@ -390,7 +496,6 @@ describe HstoreAccessor do
390
496
  product.reload
391
497
  expect(product.popular).to be true
392
498
  end
393
-
394
499
  end
395
500
 
396
501
  it "setters call the _will_change! method of the store attribute" do
@@ -400,26 +505,30 @@ describe HstoreAccessor do
400
505
 
401
506
  describe "type casting" do
402
507
  it "type casts integer values" do
403
- product.price = '468'
508
+ product.price = "468"
404
509
  expect(product.price).to eq 468
405
510
  end
511
+
406
512
  it "type casts float values" do
407
- product.weight = '93.45'
513
+ product.weight = "93.45"
408
514
  expect(product.weight).to eq 93.45
409
515
  end
516
+
410
517
  it "type casts time values" do
411
518
  timestamp = Time.now - 10.days
412
519
  product.build_timestamp = timestamp.to_s
413
520
  expect(product.build_timestamp.to_i).to eq timestamp.to_i
414
521
  end
522
+
415
523
  it "type casts date values" do
416
524
  datestamp = Date.today - 9.days
417
525
  product.released_at = datestamp.to_s
418
526
  expect(product.released_at).to eq datestamp
419
527
  end
528
+
420
529
  it "type casts decimal values" do
421
- product.miles = '1.337900129339202'
422
- expect(product.miles).to eq BigDecimal.new('1.337900129339202')
530
+ product.miles = "1.337900129339202"
531
+ expect(product.miles).to eq BigDecimal.new("1.337900129339202")
423
532
  end
424
533
 
425
534
  it "type casts boolean values" do
@@ -430,6 +539,7 @@ describe HstoreAccessor do
430
539
  product.published = value
431
540
  expect(product.published).to be true
432
541
  end
542
+
433
543
  ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.each do |value|
434
544
  product.popular = value
435
545
  expect(product.popular).to be false
@@ -480,12 +590,12 @@ describe HstoreAccessor do
480
590
  end
481
591
 
482
592
  describe "dirty tracking" do
483
-
484
593
  let(:product) { Product.new }
485
594
 
486
595
  it "<attr>_changed? should return the expected value" do
487
596
  expect(product.color_changed?).to be false
488
597
  product.color = "ORANGE"
598
+ expect(product.price_changed?).to be false
489
599
  expect(product.color_changed?).to be true
490
600
  product.save
491
601
  expect(product.color_changed?).to be false
@@ -502,20 +612,106 @@ describe HstoreAccessor do
502
612
  expect(product.price_changed?).to be false
503
613
  end
504
614
 
505
- it "<attr>_was should return the expected value" do
506
- product.color = "ORANGE"
507
- product.save
508
- product.color = "GREEN"
509
- expect(product.color_was).to eq "ORANGE"
615
+ describe "#<attr>_will_change!" do
616
+ it "tells ActiveRecord the hstore attribute has changed" do
617
+ expect(product).to receive(:options_will_change!)
618
+ product.color_will_change!
619
+ end
510
620
  end
511
621
 
512
- it "<attr>_change should return the expected value" do
513
- product.color = "ORANGE"
514
- product.save
515
- product.color = "GREEN"
516
- expect(product.color_change).to eq ["ORANGE", "GREEN"]
622
+ describe "#<attr>_was" do
623
+ it "returns the expected value" do
624
+ product.color = "ORANGE"
625
+ product.save
626
+ product.color = "GREEN"
627
+ expect(product.color_was).to eq "ORANGE"
628
+ end
629
+
630
+ it "works when the hstore attribute is nil" do
631
+ product.options = nil
632
+ product.save
633
+ product.color = "green"
634
+ expect { product.color_was }.to_not raise_error
635
+ end
517
636
  end
518
637
 
519
- end
638
+ describe "#<attr>_change" do
639
+ it "returns the old and new values" do
640
+ product.color = "ORANGE"
641
+ product.save
642
+ product.color = "GREEN"
643
+ expect(product.color_change).to eq %w(ORANGE GREEN)
644
+ end
645
+ context "when store_key differs from key" do
646
+ it "returns the old and new values" do
647
+ product.weight = 100.01
648
+ expect(product.weight_change[1]).to eq "100.01"
649
+ end
650
+ end
651
+ context "hstore attribute was nil" do
652
+ it "returns old and new values" do
653
+ product.options = nil
654
+ product.save!
655
+ green = product.color = "green"
656
+ expect(product.color_change).to eq([nil, green])
657
+ end
658
+ end
659
+
660
+ context "other hstore attributes were persisted" do
661
+ it "returns nil" do
662
+ product.price = 5
663
+ product.save!
664
+ product.price = 6
665
+ expect(product.color_change).to be_nil
666
+ end
667
+ end
668
+
669
+ context "not persisted" do
670
+ it "returns nil when there are no changes" do
671
+ expect(product.color_change).to be_nil
672
+ end
673
+ end
674
+ end
520
675
 
676
+ describe "#reset_<attr>!" do
677
+ before do
678
+ allow(ActiveSupport::Deprecation).to receive(:warn)
679
+ end
680
+
681
+ if ActiveRecord::VERSION::STRING.to_f >= 4.2
682
+ it "displays a deprecation warning" do
683
+ expect(ActiveSupport::Deprecation).to receive(:warn)
684
+ product.reset_color!
685
+ end
686
+ else
687
+ it "does not display a deprecation warning" do
688
+ expect(ActiveSupport::Deprecation).to_not receive(:warn)
689
+ product.reset_color!
690
+ end
691
+ end
692
+
693
+ it "restores the attribute" do
694
+ expect(product).to receive(:restore_color!)
695
+ product.reset_color!
696
+ end
697
+ end
698
+
699
+ describe "#restore_<attr>!" do
700
+ it "restores the attribute" do
701
+ product.color = "red"
702
+ product.restore_color!
703
+ expect(product.color).to be_nil
704
+ end
705
+
706
+ context "persisted" do
707
+ it "restores the attribute" do
708
+ green = product.color = "green"
709
+ product.save!
710
+ product.color = "red"
711
+ product.restore_color!
712
+ expect(product.color).to eq(green)
713
+ end
714
+ end
715
+ end
716
+ end
521
717
  end