hstore_accessor 0.6.1 → 0.9.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.
@@ -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