hstore_accessor_rails5 1.0.4
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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rubocop.yml +43 -0
- data/.ruby-version +1 -0
- data/Appraisals +11 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +291 -0
- data/Rakefile +10 -0
- data/gemfiles/activerecord_4.0.gemfile +11 -0
- data/gemfiles/activerecord_4.1.gemfile +11 -0
- data/gemfiles/activerecord_4.2.gemfile +11 -0
- data/hstore_accessor.gemspec +34 -0
- data/lib/hstore_accessor.rb +27 -0
- data/lib/hstore_accessor/active_record_4.2/type_helpers.rb +34 -0
- data/lib/hstore_accessor/active_record_5.0/type_helpers.rb +34 -0
- data/lib/hstore_accessor/active_record_pre_4.2/time_helper.rb +29 -0
- data/lib/hstore_accessor/active_record_pre_4.2/type_helpers.rb +42 -0
- data/lib/hstore_accessor/macro.rb +154 -0
- data/lib/hstore_accessor/serialization.rb +49 -0
- data/lib/hstore_accessor/version.rb +3 -0
- data/spec/hstore_accessor_spec.rb +709 -0
- data/spec/spec_helper.rb +48 -0
- metadata +217 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "hstore_accessor/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "hstore_accessor_rails5"
|
8
|
+
spec.version = HstoreAccessor::VERSION
|
9
|
+
spec.authors = ["Joe Hirn", "Cory Stephenson", "JC Grubbs", "Tony Coconate", "Michael Crismali"]
|
10
|
+
spec.email = ["joe@devmynd.com", "cory@devmynd.com", "jc@devmynd.com", "me@tonycoconate.com", "michael@devmynd.com"]
|
11
|
+
spec.description = "fork of hstore accessor that supports rails 5 ."
|
12
|
+
spec.summary = "Adds typed hstore backed fields to an ActiveRecord model."
|
13
|
+
spec.homepage = "http://github.com/devmynd/hstore_accessor"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activerecord", ">= 4.0.0"
|
22
|
+
|
23
|
+
spec.add_development_dependency "appraisal"
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
25
|
+
spec.add_development_dependency "database_cleaner"
|
26
|
+
spec.add_development_dependency "pry"
|
27
|
+
spec.add_development_dependency "pry-doc"
|
28
|
+
spec.add_development_dependency "pry-nav"
|
29
|
+
spec.add_development_dependency "rake"
|
30
|
+
spec.add_development_dependency "rspec", "~> 3.1.0"
|
31
|
+
spec.add_development_dependency "rubocop"
|
32
|
+
|
33
|
+
spec.post_install_message = "Please note that the `array` and `hash` types are no longer supported in version 1.0.0"
|
34
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "active_support"
|
2
|
+
require "active_record"
|
3
|
+
require "hstore_accessor/version"
|
4
|
+
|
5
|
+
if ::ActiveRecord::VERSION::STRING.to_f >= 5.0
|
6
|
+
require "hstore_accessor/active_record_5.0/type_helpers"
|
7
|
+
elsif ::ActiveRecord::VERSION::STRING.to_f >= 4.2 && ::ActiveRecord::VERSION::STRING.to_f < 5.0
|
8
|
+
require "hstore_accessor/active_record_4.2/type_helpers"
|
9
|
+
else
|
10
|
+
require "hstore_accessor/active_record_pre_4.2/type_helpers"
|
11
|
+
require "hstore_accessor/active_record_pre_4.2/time_helper"
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
require "hstore_accessor/serialization"
|
16
|
+
require "hstore_accessor/macro"
|
17
|
+
require "bigdecimal"
|
18
|
+
|
19
|
+
module HstoreAccessor
|
20
|
+
extend ActiveSupport::Concern
|
21
|
+
include Serialization
|
22
|
+
include Macro
|
23
|
+
end
|
24
|
+
|
25
|
+
ActiveSupport.on_load(:active_record) do
|
26
|
+
ActiveRecord::Base.send(:include, HstoreAccessor)
|
27
|
+
end
|
@@ -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, :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
|
@@ -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, :decimal
|
25
|
+
value
|
26
|
+
when :integer, :float, :datetime, :date, :boolean
|
27
|
+
TYPES[type].new.cast(value)
|
28
|
+
else value
|
29
|
+
# Nothing.
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module HstoreAccessor
|
2
|
+
module TimeHelper
|
3
|
+
# There is a bug in ActiveRecord::ConnectionAdapters::Column#string_to_time
|
4
|
+
# which drops the timezone. This has been fixed, but not released.
|
5
|
+
# This method includes the fix. See: https://github.com/rails/rails/pull/12290
|
6
|
+
|
7
|
+
def self.string_to_time(string)
|
8
|
+
return string unless string.is_a?(String)
|
9
|
+
return nil if string.empty?
|
10
|
+
|
11
|
+
time_hash = Date._parse(string)
|
12
|
+
time_hash[:sec_fraction] = ActiveRecord::ConnectionAdapters::Column.send(:microseconds, time_hash)
|
13
|
+
year, mon, mday, hour, min, sec, microsec, offset = time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)
|
14
|
+
|
15
|
+
# Treat 0000-00-00 00:00:00 as nil.
|
16
|
+
return nil if year.nil? || [year, mon, mday].all?(&:zero?)
|
17
|
+
|
18
|
+
if offset
|
19
|
+
time = Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
|
20
|
+
return nil unless time
|
21
|
+
|
22
|
+
time -= offset
|
23
|
+
ActiveRecord::Base.default_timezone == :utc ? time : time.getlocal
|
24
|
+
else
|
25
|
+
Time.public_send(ActiveRecord::Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
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, :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
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module HstoreAccessor
|
2
|
+
module Macro
|
3
|
+
module ClassMethods
|
4
|
+
def hstore_accessor(hstore_attribute, 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
|
13
|
+
end
|
14
|
+
|
15
|
+
field_methods = Module.new
|
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|
|
47
|
+
data_type = type
|
48
|
+
store_key = key
|
49
|
+
|
50
|
+
if type.is_a?(Hash)
|
51
|
+
type = type.with_indifferent_access
|
52
|
+
data_type = type[:data_type]
|
53
|
+
store_key = type[:store_key]
|
54
|
+
end
|
55
|
+
|
56
|
+
data_type = data_type.to_sym
|
57
|
+
|
58
|
+
raise Serialization::InvalidDataTypeError unless Serialization::VALID_TYPES.include?(data_type)
|
59
|
+
|
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))
|
72
|
+
end
|
73
|
+
|
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
|
78
|
+
|
79
|
+
define_method("#{key}?") do
|
80
|
+
send(key).present?
|
81
|
+
end
|
82
|
+
|
83
|
+
define_method("#{key}_changed?") do
|
84
|
+
send("#{key}_change").present?
|
85
|
+
end
|
86
|
+
|
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.uniq.size == 1 ? nil : attribute_changes
|
96
|
+
end
|
97
|
+
|
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
|
116
|
+
end
|
117
|
+
|
118
|
+
query_field = "#{table_name}.#{hstore_attribute} -> '#{store_key}'"
|
119
|
+
eq_query_field = "#{table_name}.#{hstore_attribute} @> hstore('#{store_key}', ?)"
|
120
|
+
|
121
|
+
case data_type
|
122
|
+
when :string
|
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) })
|
132
|
+
send(:scope, "#{key}_lte", -> value { where("(#{query_field})::#{data_type} <= ?", value.to_s) })
|
133
|
+
send(:scope, "#{key}_eq", -> value { where("(#{query_field})::#{data_type} = ?", value.to_s) })
|
134
|
+
send(:scope, "#{key}_gte", -> value { where("(#{query_field})::#{data_type} >= ?", value.to_s) })
|
135
|
+
send(:scope, "#{key}_gt", -> value { where("(#{query_field})::#{data_type} > ?", value.to_s) })
|
136
|
+
when :datetime
|
137
|
+
send(:scope, "#{key}_before", -> 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) })
|
140
|
+
when :date
|
141
|
+
send(:scope, "#{key}_before", -> 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) })
|
144
|
+
when :boolean
|
145
|
+
send(:scope, "is_#{key}", -> { where(eq_query_field, "true") })
|
146
|
+
send(:scope, "not_#{key}", -> { where(eq_query_field, "false") })
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
include field_methods
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module HstoreAccessor
|
2
|
+
module Serialization
|
3
|
+
InvalidDataTypeError = Class.new(StandardError)
|
4
|
+
|
5
|
+
VALID_TYPES = [
|
6
|
+
:boolean,
|
7
|
+
:date,
|
8
|
+
:datetime,
|
9
|
+
:decimal,
|
10
|
+
:float,
|
11
|
+
:integer,
|
12
|
+
:string
|
13
|
+
]
|
14
|
+
|
15
|
+
DEFAULT_SERIALIZER = ->(value) { value.to_s }
|
16
|
+
DEFAULT_DESERIALIZER = DEFAULT_SERIALIZER
|
17
|
+
|
18
|
+
SERIALIZERS = {
|
19
|
+
boolean: -> value { (value.to_s == "true").to_s },
|
20
|
+
date: -> value { value && value.to_s },
|
21
|
+
datetime: -> value { value && value.to_i }
|
22
|
+
}
|
23
|
+
SERIALIZERS.default = DEFAULT_SERIALIZER
|
24
|
+
|
25
|
+
DESERIALIZERS = {
|
26
|
+
boolean: -> value { TypeHelpers.cast(:boolean, value) },
|
27
|
+
date: -> value { value && Date.parse(value) },
|
28
|
+
decimal: -> value { value && BigDecimal.new(value) },
|
29
|
+
float: -> value { value && value.to_f },
|
30
|
+
integer: -> value { value && value.to_i },
|
31
|
+
datetime: -> value { value && Time.at(value.to_i).in_time_zone }
|
32
|
+
}
|
33
|
+
DESERIALIZERS.default = DEFAULT_DESERIALIZER
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def serialize(type, value, serializer=SERIALIZERS[type])
|
37
|
+
return nil if value.nil?
|
38
|
+
|
39
|
+
serializer.call(value)
|
40
|
+
end
|
41
|
+
|
42
|
+
def deserialize(type, value, deserializer=DESERIALIZERS[type])
|
43
|
+
return nil if value.nil?
|
44
|
+
|
45
|
+
deserializer.call(value)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,709 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "active_support/all"
|
3
|
+
|
4
|
+
FIELDS = {
|
5
|
+
name: :string,
|
6
|
+
color: :string,
|
7
|
+
price: :integer,
|
8
|
+
published: { data_type: :boolean, store_key: "p" },
|
9
|
+
weight: { data_type: :float, store_key: "w" },
|
10
|
+
popular: :boolean,
|
11
|
+
build_timestamp: :datetime,
|
12
|
+
released_at: :date,
|
13
|
+
likes: :integer,
|
14
|
+
miles: :decimal
|
15
|
+
}
|
16
|
+
|
17
|
+
DATA_FIELDS = {
|
18
|
+
color_data: :string
|
19
|
+
}
|
20
|
+
|
21
|
+
class ProductCategory < ActiveRecord::Base
|
22
|
+
hstore_accessor :options, name: :string, likes: :integer
|
23
|
+
has_many :products
|
24
|
+
end
|
25
|
+
|
26
|
+
class Product < ActiveRecord::Base
|
27
|
+
hstore_accessor :options, FIELDS
|
28
|
+
hstore_accessor :data, DATA_FIELDS
|
29
|
+
belongs_to :product_category
|
30
|
+
end
|
31
|
+
|
32
|
+
class SuperProduct < Product
|
33
|
+
end
|
34
|
+
|
35
|
+
describe HstoreAccessor do
|
36
|
+
context "macro" do
|
37
|
+
let(:product) { Product.new }
|
38
|
+
|
39
|
+
FIELDS.keys.each do |field|
|
40
|
+
it "creates a getter for the hstore field: #{field}" do
|
41
|
+
expect(product).to respond_to(field)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "creates a setter for the hstore field: #{field}=" do
|
45
|
+
expect(product).to respond_to(:"#{field}=")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it "raises an InvalidDataTypeError if an invalid type is specified" do
|
50
|
+
expect do
|
51
|
+
class FakeModel
|
52
|
+
include HstoreAccessor
|
53
|
+
hstore_accessor :foo, bar: :baz
|
54
|
+
end
|
55
|
+
end.to raise_error(HstoreAccessor::InvalidDataTypeError)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "stores using the store_key if one is provided" do
|
59
|
+
product.weight = 38.5
|
60
|
+
product.save
|
61
|
+
product.reload
|
62
|
+
expect(product.options["w"]).to eq "38.5"
|
63
|
+
expect(product.weight).to eq 38.5
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "#hstore_metadata_for_*" do
|
68
|
+
let(:product) { Product }
|
69
|
+
|
70
|
+
it "returns the metadata hash for the specified field" do
|
71
|
+
expect(product.hstore_metadata_for_options).to eq FIELDS
|
72
|
+
expect(product.hstore_metadata_for_data).to eq DATA_FIELDS
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context "nil values" do
|
77
|
+
let!(:timestamp) { Time.now }
|
78
|
+
let!(:datestamp) { Date.today }
|
79
|
+
let(:product) { Product.new }
|
80
|
+
let(:persisted_product) { Product.create!(color: "green", price: 10, weight: 10.1, popular: true, build_timestamp: (timestamp - 10.days), released_at: (datestamp - 8.days), miles: BigDecimal.new("9.133790001")) }
|
81
|
+
|
82
|
+
FIELDS.keys.each do |field|
|
83
|
+
it "responds with nil when #{field} is not set" do
|
84
|
+
expect(product.send(field)).to be_nil
|
85
|
+
end
|
86
|
+
|
87
|
+
it "responds with nil when #{field} is set back to nil after being set initially" do
|
88
|
+
persisted_product.send("#{field}=", nil)
|
89
|
+
expect(persisted_product.send(field)).to be_nil
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "predicate methods" do
|
95
|
+
let!(:product) { Product.new }
|
96
|
+
|
97
|
+
it "exist for each field" do
|
98
|
+
FIELDS.keys.each do |field|
|
99
|
+
expect(product).to respond_to "#{field}?"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
it "uses 'present?' to determine return value" do
|
104
|
+
stub = double(present?: :result_of_present)
|
105
|
+
expect(stub).to receive(:present?)
|
106
|
+
allow(product).to receive_messages(color: stub)
|
107
|
+
expect(product.color?).to eq(:result_of_present)
|
108
|
+
end
|
109
|
+
|
110
|
+
context "boolean fields" do
|
111
|
+
it "return the state for true boolean fields" do
|
112
|
+
product.popular = true
|
113
|
+
product.save
|
114
|
+
product.reload
|
115
|
+
expect(product.popular?).to be true
|
116
|
+
end
|
117
|
+
|
118
|
+
it "return the state for false boolean fields" do
|
119
|
+
product.popular = false
|
120
|
+
product.save
|
121
|
+
product.reload
|
122
|
+
expect(product.popular?).to be false
|
123
|
+
end
|
124
|
+
|
125
|
+
it "return true for boolean field set via hash using real boolean" do
|
126
|
+
product.options = { "popular" => true }
|
127
|
+
expect(product.popular?).to be true
|
128
|
+
end
|
129
|
+
|
130
|
+
it "return false for boolean field set via hash using real boolean" do
|
131
|
+
product.options = { "popular" => false }
|
132
|
+
expect(product.popular?).to be false
|
133
|
+
end
|
134
|
+
|
135
|
+
it "return true for boolean field set via hash using string" do
|
136
|
+
product.options = { "popular" => "true" }
|
137
|
+
expect(product.popular?).to be true
|
138
|
+
end
|
139
|
+
|
140
|
+
it "return false for boolean field set via hash using string" do
|
141
|
+
product.options = { "popular" => "false" }
|
142
|
+
expect(product.popular?).to be false
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe "scopes" do
|
148
|
+
let!(:timestamp) { Time.now }
|
149
|
+
let!(:datestamp) { Date.today }
|
150
|
+
let!(:product_a) { Product.create(likes: 3, name: "widget", color: "green", price: 10, weight: 10.1, 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, 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, popular: true, build_timestamp: timestamp, released_at: datestamp, miles: BigDecimal.new("30.313379001")) }
|
153
|
+
|
154
|
+
context "ambiguous column names" do
|
155
|
+
let!(:product_category) { ProductCategory.create!(name: "widget", likes: 2) }
|
156
|
+
|
157
|
+
before do
|
158
|
+
Product.all.to_a.each do |product|
|
159
|
+
product_category.products << product
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context "eq query" do
|
164
|
+
let!(:query) { Product.all.joins(:product_category).merge(ProductCategory.with_name("widget")).with_name("widget") }
|
165
|
+
|
166
|
+
it "qualifies the table name to prevent ambiguous column name references" do
|
167
|
+
expect { query.to_a }.to_not raise_error
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
context "query" do
|
172
|
+
let!(:query) { Product.all.joins(:product_category).merge(ProductCategory.likes_lt(4)).likes_lt(4) }
|
173
|
+
|
174
|
+
it "qualifies the table name to prevent ambiguous column name references" do
|
175
|
+
expect { query.to_a }.to_not raise_error
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
context "for string fields support" do
|
181
|
+
it "equality" do
|
182
|
+
expect(Product.with_color("orange").to_a).to eq [product_b]
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
context "for integer fields support" do
|
187
|
+
it "less than" do
|
188
|
+
expect(Product.price_lt(20).to_a).to eq [product_a]
|
189
|
+
end
|
190
|
+
|
191
|
+
it "less than or equal" do
|
192
|
+
expect(Product.price_lte(20).to_a).to eq [product_a, product_b]
|
193
|
+
end
|
194
|
+
|
195
|
+
it "equality" do
|
196
|
+
expect(Product.price_eq(10).to_a).to eq [product_a]
|
197
|
+
end
|
198
|
+
|
199
|
+
it "greater than or equal" do
|
200
|
+
expect(Product.price_gte(20).to_a).to eq [product_b, product_c]
|
201
|
+
end
|
202
|
+
|
203
|
+
it "greater than" do
|
204
|
+
expect(Product.price_gt(20).to_a).to eq [product_c]
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
context "for float fields support" do
|
209
|
+
it "less than" do
|
210
|
+
expect(Product.weight_lt(20.0).to_a).to eq [product_a]
|
211
|
+
end
|
212
|
+
|
213
|
+
it "less than or equal" do
|
214
|
+
expect(Product.weight_lte(20.2).to_a).to eq [product_a, product_b]
|
215
|
+
end
|
216
|
+
|
217
|
+
it "equality" do
|
218
|
+
expect(Product.weight_eq(10.1).to_a).to eq [product_a]
|
219
|
+
end
|
220
|
+
|
221
|
+
it "greater than or equal" do
|
222
|
+
expect(Product.weight_gte(20.2).to_a).to eq [product_b, product_c]
|
223
|
+
end
|
224
|
+
|
225
|
+
it "greater than" do
|
226
|
+
expect(Product.weight_gt(20.5).to_a).to eq [product_c]
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
context "for decimal fields support" do
|
231
|
+
it "less than" do
|
232
|
+
expect(Product.miles_lt(BigDecimal.new("10.55555")).to_a).to eq [product_a]
|
233
|
+
end
|
234
|
+
|
235
|
+
it "less than or equal" do
|
236
|
+
expect(Product.miles_lte(BigDecimal.new("20.213379001")).to_a).to eq [product_a, product_b]
|
237
|
+
end
|
238
|
+
|
239
|
+
it "equality" do
|
240
|
+
expect(Product.miles_eq(BigDecimal.new("10.113379001")).to_a).to eq [product_a]
|
241
|
+
end
|
242
|
+
|
243
|
+
it "greater than or equal" do
|
244
|
+
expect(Product.miles_gte(BigDecimal.new("20.213379001")).to_a).to eq [product_b, product_c]
|
245
|
+
end
|
246
|
+
|
247
|
+
it "greater than" do
|
248
|
+
expect(Product.miles_gt(BigDecimal.new("20.555555")).to_a).to eq [product_c]
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
context "for datetime fields support" do
|
253
|
+
it "before" do
|
254
|
+
expect(Product.build_timestamp_before(timestamp)).to eq [product_a, product_b]
|
255
|
+
end
|
256
|
+
|
257
|
+
it "equality" do
|
258
|
+
expect(Product.build_timestamp_eq(timestamp)).to eq [product_c]
|
259
|
+
end
|
260
|
+
|
261
|
+
it "after" do
|
262
|
+
expect(Product.build_timestamp_after(timestamp - 6.days)).to eq [product_b, product_c]
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
context "for date fields support" do
|
267
|
+
it "before" do
|
268
|
+
expect(Product.released_at_before(datestamp)).to eq [product_a, product_b]
|
269
|
+
end
|
270
|
+
|
271
|
+
it "equality" do
|
272
|
+
expect(Product.released_at_eq(datestamp)).to eq [product_c]
|
273
|
+
end
|
274
|
+
|
275
|
+
it "after" do
|
276
|
+
expect(Product.released_at_after(datestamp - 6.days)).to eq [product_b, product_c]
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
context "for boolean field support" do
|
281
|
+
it "true" do
|
282
|
+
expect(Product.is_popular).to eq [product_a, product_c]
|
283
|
+
end
|
284
|
+
|
285
|
+
it "false" do
|
286
|
+
expect(Product.not_popular).to eq [product_b]
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
describe "#type_for_attribute" do
|
292
|
+
if ::ActiveRecord::VERSION::STRING.to_f >= 4.2
|
293
|
+
subject { SuperProduct }
|
294
|
+
|
295
|
+
def self.it_returns_the_type_for_the_attribute(type, attribute_name, active_record_type)
|
296
|
+
context "#{type}" do
|
297
|
+
it "returns the type for the column" do
|
298
|
+
expect(subject.type_for_attribute(attribute_name.to_s)).to eq(active_record_type.new)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
it_returns_the_type_for_the_attribute "default behavior", :string_type, ActiveRecord::Type::String
|
304
|
+
it_returns_the_type_for_the_attribute :string, :color, ActiveRecord::Type::String
|
305
|
+
it_returns_the_type_for_the_attribute :integer, :price, ActiveRecord::Type::Integer
|
306
|
+
it_returns_the_type_for_the_attribute :float, :weight, ActiveRecord::Type::Float
|
307
|
+
it_returns_the_type_for_the_attribute :datetime, :build_timestamp, ActiveRecord::Type::DateTime
|
308
|
+
it_returns_the_type_for_the_attribute :date, :released_at, ActiveRecord::Type::Date
|
309
|
+
it_returns_the_type_for_the_attribute :boolean, :published, ActiveRecord::Type::Boolean
|
310
|
+
else
|
311
|
+
subject { Product }
|
312
|
+
|
313
|
+
it "is not defined" do
|
314
|
+
expect(subject).to_not respond_to(:type_for_attribute)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
describe "#column_for_attribute" do
|
320
|
+
if ActiveRecord::VERSION::STRING.to_f >= 4.2
|
321
|
+
|
322
|
+
def self.it_returns_the_properly_typed_column(type, attribute_name, cast_type_class)
|
323
|
+
context "#{type}" do
|
324
|
+
subject { SuperProduct.column_for_attribute(attribute_name) }
|
325
|
+
it "returns a column with a #{type} cast type" do
|
326
|
+
expect(subject).to be_a(ActiveRecord::ConnectionAdapters::Column)
|
327
|
+
expect(subject.cast_type).to eq(cast_type_class.new)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
it_returns_the_properly_typed_column :string, :color, ActiveRecord::Type::String
|
333
|
+
it_returns_the_properly_typed_column :integer, :price, ActiveRecord::Type::Integer
|
334
|
+
it_returns_the_properly_typed_column :boolean, :published, ActiveRecord::Type::Boolean
|
335
|
+
it_returns_the_properly_typed_column :float, :weight, ActiveRecord::Type::Float
|
336
|
+
it_returns_the_properly_typed_column :datetime, :build_timestamp, ActiveRecord::Type::DateTime
|
337
|
+
it_returns_the_properly_typed_column :date, :released_at, ActiveRecord::Type::Date
|
338
|
+
it_returns_the_properly_typed_column :decimal, :miles, ActiveRecord::Type::Decimal
|
339
|
+
else
|
340
|
+
def self.it_returns_the_properly_typed_column(hstore_type, attribute_name, active_record_type)
|
341
|
+
context "#{hstore_type}" do
|
342
|
+
subject { SuperProduct.new.column_for_attribute(attribute_name) }
|
343
|
+
it "returns a column with a #{hstore_type} cast type" do
|
344
|
+
expect(subject).to be_a(ActiveRecord::ConnectionAdapters::Column)
|
345
|
+
expect(subject.type).to eq(active_record_type)
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
it_returns_the_properly_typed_column :string, :color, :string
|
351
|
+
it_returns_the_properly_typed_column :integer, :price, :integer
|
352
|
+
it_returns_the_properly_typed_column :boolean, :published, :boolean
|
353
|
+
it_returns_the_properly_typed_column :float, :weight, :float
|
354
|
+
it_returns_the_properly_typed_column :time, :build_timestamp, :datetime
|
355
|
+
it_returns_the_properly_typed_column :date, :released_at, :date
|
356
|
+
it_returns_the_properly_typed_column :decimal, :miles, :decimal
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
context "when assigning values it" do
|
361
|
+
let(:product) { Product.new }
|
362
|
+
|
363
|
+
it "correctly stores string values" do
|
364
|
+
product.color = "blue"
|
365
|
+
product.save
|
366
|
+
product.reload
|
367
|
+
expect(product.color).to eq "blue"
|
368
|
+
end
|
369
|
+
|
370
|
+
it "allows access to bulk set values via string before saving" do
|
371
|
+
product.options = {
|
372
|
+
"color" => "blue",
|
373
|
+
"price" => 120
|
374
|
+
}
|
375
|
+
expect(product.color).to eq "blue"
|
376
|
+
expect(product.price).to eq 120
|
377
|
+
end
|
378
|
+
|
379
|
+
it "allows access to bulk set values via :symbols before saving" do
|
380
|
+
product.options = {
|
381
|
+
color: "blue",
|
382
|
+
price: 120
|
383
|
+
}
|
384
|
+
expect(product.color).to eq "blue"
|
385
|
+
expect(product.price).to eq 120
|
386
|
+
end
|
387
|
+
|
388
|
+
it "correctly stores integer values" do
|
389
|
+
product.price = 468
|
390
|
+
product.save
|
391
|
+
product.reload
|
392
|
+
expect(product.price).to eq 468
|
393
|
+
end
|
394
|
+
|
395
|
+
it "correctly stores float values" do
|
396
|
+
product.weight = 93.45
|
397
|
+
product.save
|
398
|
+
product.reload
|
399
|
+
expect(product.weight).to eq 93.45
|
400
|
+
end
|
401
|
+
|
402
|
+
context "multipart values" do
|
403
|
+
it "stores multipart dates correctly" do
|
404
|
+
product.update_attributes!(
|
405
|
+
"released_at(1i)" => "2014",
|
406
|
+
"released_at(2i)" => "04",
|
407
|
+
"released_at(3i)" => "14"
|
408
|
+
)
|
409
|
+
product.reload
|
410
|
+
expect(product.released_at).to eq(Date.new(2014, 4, 14))
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
context "time values" do
|
415
|
+
let(:chicago_timezone) { ActiveSupport::TimeZone["America/Chicago"] }
|
416
|
+
let(:new_york_timezone) { ActiveSupport::TimeZone["America/New_York"] }
|
417
|
+
|
418
|
+
it "correctly stores value" do
|
419
|
+
timestamp = Time.now - 10.days
|
420
|
+
product.build_timestamp = timestamp
|
421
|
+
product.save!
|
422
|
+
product.reload
|
423
|
+
expect(product.build_timestamp.to_i).to eq timestamp.to_i
|
424
|
+
end
|
425
|
+
|
426
|
+
it "stores the value in utc" do
|
427
|
+
timestamp = Time.now.in_time_zone(chicago_timezone) - 10.days
|
428
|
+
product.build_timestamp = timestamp
|
429
|
+
product.save!
|
430
|
+
product.reload
|
431
|
+
expect(product.options["build_timestamp"].to_i).to eq timestamp.utc.to_i
|
432
|
+
end
|
433
|
+
|
434
|
+
it "returns the time value in the current time zone" do
|
435
|
+
timestamp = Time.now.in_time_zone(chicago_timezone) - 10.days
|
436
|
+
product.build_timestamp = timestamp
|
437
|
+
product.save!
|
438
|
+
product.reload
|
439
|
+
Time.use_zone(new_york_timezone) do
|
440
|
+
expect(product.build_timestamp.to_s).to eq timestamp.in_time_zone(new_york_timezone).to_s
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
it "correctly stores date values" do
|
446
|
+
datestamp = Date.today - 9.days
|
447
|
+
product.released_at = datestamp
|
448
|
+
product.save
|
449
|
+
product.reload
|
450
|
+
expect(product.released_at.to_s).to eq datestamp.to_s
|
451
|
+
expect(product.released_at).to eq datestamp
|
452
|
+
end
|
453
|
+
|
454
|
+
it "correctly stores decimal values" do
|
455
|
+
decimal = BigDecimal.new("9.13370009001")
|
456
|
+
product.miles = decimal
|
457
|
+
product.save
|
458
|
+
product.reload
|
459
|
+
expect(product.miles.to_s).to eq decimal.to_s
|
460
|
+
expect(product.miles).to eq decimal
|
461
|
+
end
|
462
|
+
|
463
|
+
context "correctly stores boolean values" do
|
464
|
+
it "when string 'true' is passed" do
|
465
|
+
product.popular = "true"
|
466
|
+
product.save
|
467
|
+
product.reload
|
468
|
+
expect(product.popular).to be true
|
469
|
+
end
|
470
|
+
|
471
|
+
it "when a real boolean is passed" do
|
472
|
+
product.popular = true
|
473
|
+
product.save
|
474
|
+
product.reload
|
475
|
+
expect(product.popular).to be true
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
it "setters call the _will_change! method of the store attribute" do
|
480
|
+
expect(product).to receive(:options_will_change!)
|
481
|
+
product.color = "green"
|
482
|
+
end
|
483
|
+
|
484
|
+
describe "type casting" do
|
485
|
+
it "type casts integer values" do
|
486
|
+
product.price = "468"
|
487
|
+
expect(product.price).to eq 468
|
488
|
+
end
|
489
|
+
|
490
|
+
it "type casts float values" do
|
491
|
+
product.weight = "93.45"
|
492
|
+
expect(product.weight).to eq 93.45
|
493
|
+
end
|
494
|
+
|
495
|
+
it "type casts time values" do
|
496
|
+
timestamp = Time.now - 10.days
|
497
|
+
product.build_timestamp = timestamp.to_s
|
498
|
+
expect(product.build_timestamp.to_i).to eq timestamp.to_i
|
499
|
+
end
|
500
|
+
|
501
|
+
it "type casts date values" do
|
502
|
+
datestamp = Date.today - 9.days
|
503
|
+
product.released_at = datestamp.to_s
|
504
|
+
expect(product.released_at).to eq datestamp
|
505
|
+
end
|
506
|
+
|
507
|
+
it "type casts decimal values" do
|
508
|
+
product.miles = "1.337900129339202"
|
509
|
+
expect(product.miles).to eq BigDecimal.new("1.337900129339202")
|
510
|
+
end
|
511
|
+
|
512
|
+
it "type casts boolean values" do
|
513
|
+
ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.each do |value|
|
514
|
+
product.popular = value
|
515
|
+
expect(product.popular).to be true
|
516
|
+
|
517
|
+
product.published = value
|
518
|
+
expect(product.published).to be true
|
519
|
+
end
|
520
|
+
|
521
|
+
ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.each do |value|
|
522
|
+
product.popular = value
|
523
|
+
expect(product.popular).to be false
|
524
|
+
|
525
|
+
product.published = value
|
526
|
+
expect(product.published).to be false
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
context "extended getters and setters" do
|
532
|
+
before do
|
533
|
+
class Product
|
534
|
+
alias_method :set_color, :color=
|
535
|
+
alias_method :get_color, :color
|
536
|
+
|
537
|
+
def color=(value)
|
538
|
+
super(value.upcase)
|
539
|
+
end
|
540
|
+
|
541
|
+
def color
|
542
|
+
super.try(:downcase)
|
543
|
+
end
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
after do
|
548
|
+
class Product
|
549
|
+
alias_method :color=, :set_color
|
550
|
+
alias_method :color, :get_color
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
context "setters" do
|
555
|
+
it "can be wrapped" do
|
556
|
+
product.color = "red"
|
557
|
+
expect(product.options["color"]).to eq("RED")
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
context "getters" do
|
562
|
+
it "can be wrapped" do
|
563
|
+
product.color = "GREEN"
|
564
|
+
expect(product.color).to eq("green")
|
565
|
+
end
|
566
|
+
end
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
describe "dirty tracking" do
|
571
|
+
let(:product) { Product.new }
|
572
|
+
|
573
|
+
it "<attr>_changed? should return the expected value" do
|
574
|
+
expect(product.color_changed?).to be false
|
575
|
+
product.color = "ORANGE"
|
576
|
+
expect(product.price_changed?).to be false
|
577
|
+
expect(product.color_changed?).to be true
|
578
|
+
product.save
|
579
|
+
expect(product.color_changed?).to be false
|
580
|
+
product.color = "ORANGE"
|
581
|
+
expect(product.color_changed?).to be false
|
582
|
+
|
583
|
+
expect(product.price_changed?).to be false
|
584
|
+
product.price = 100
|
585
|
+
expect(product.price_changed?).to be true
|
586
|
+
product.save
|
587
|
+
expect(product.price_changed?).to be false
|
588
|
+
product.price = "100"
|
589
|
+
expect(product.price).to be 100
|
590
|
+
expect(product.price_changed?).to be false
|
591
|
+
end
|
592
|
+
|
593
|
+
describe "#<attr>_will_change!" do
|
594
|
+
it "tells ActiveRecord the hstore attribute has changed" do
|
595
|
+
expect(product).to receive(:options_will_change!)
|
596
|
+
product.color_will_change!
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
describe "#<attr>_was" do
|
601
|
+
it "returns the expected value" do
|
602
|
+
product.color = "ORANGE"
|
603
|
+
product.save
|
604
|
+
product.color = "GREEN"
|
605
|
+
expect(product.color_was).to eq "ORANGE"
|
606
|
+
end
|
607
|
+
|
608
|
+
it "works when the hstore attribute is nil" do
|
609
|
+
product.options = nil
|
610
|
+
product.save
|
611
|
+
product.color = "green"
|
612
|
+
expect { product.color_was }.to_not raise_error
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
describe "#<attr>_change" do
|
617
|
+
it "returns the old and new values" do
|
618
|
+
product.color = "ORANGE"
|
619
|
+
product.save
|
620
|
+
product.color = "GREEN"
|
621
|
+
expect(product.color_change).to eq %w(ORANGE GREEN)
|
622
|
+
end
|
623
|
+
|
624
|
+
context "when store_key differs from key" do
|
625
|
+
it "returns the old and new values" do
|
626
|
+
product.weight = 100.01
|
627
|
+
expect(product.weight_change[1]).to eq "100.01"
|
628
|
+
end
|
629
|
+
end
|
630
|
+
|
631
|
+
context "hstore attribute was nil" do
|
632
|
+
it "returns old and new values" do
|
633
|
+
product.options = nil
|
634
|
+
product.save!
|
635
|
+
green = product.color = "green"
|
636
|
+
expect(product.color_change).to eq([nil, green])
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
context "other hstore attributes were persisted" do
|
641
|
+
it "returns nil" do
|
642
|
+
product.price = 5
|
643
|
+
product.save!
|
644
|
+
product.price = 6
|
645
|
+
expect(product.color_change).to be_nil
|
646
|
+
end
|
647
|
+
|
648
|
+
it "returns nil when other attributes were changed" do
|
649
|
+
product.price = 5
|
650
|
+
product.save!
|
651
|
+
product = Product.first
|
652
|
+
|
653
|
+
expect(product.price_change).to be_nil
|
654
|
+
|
655
|
+
product.color = "red"
|
656
|
+
|
657
|
+
expect(product.price_change).to be_nil
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
context "not persisted" do
|
662
|
+
it "returns nil when there are no changes" do
|
663
|
+
expect(product.color_change).to be_nil
|
664
|
+
end
|
665
|
+
end
|
666
|
+
end
|
667
|
+
|
668
|
+
describe "#reset_<attr>!" do
|
669
|
+
before do
|
670
|
+
allow(ActiveSupport::Deprecation).to receive(:warn)
|
671
|
+
end
|
672
|
+
|
673
|
+
if ActiveRecord::VERSION::STRING.to_f >= 4.2
|
674
|
+
it "displays a deprecation warning" do
|
675
|
+
expect(ActiveSupport::Deprecation).to receive(:warn)
|
676
|
+
product.reset_color!
|
677
|
+
end
|
678
|
+
else
|
679
|
+
it "does not display a deprecation warning" do
|
680
|
+
expect(ActiveSupport::Deprecation).to_not receive(:warn)
|
681
|
+
product.reset_color!
|
682
|
+
end
|
683
|
+
end
|
684
|
+
|
685
|
+
it "restores the attribute" do
|
686
|
+
expect(product).to receive(:restore_color!)
|
687
|
+
product.reset_color!
|
688
|
+
end
|
689
|
+
end
|
690
|
+
|
691
|
+
describe "#restore_<attr>!" do
|
692
|
+
it "restores the attribute" do
|
693
|
+
product.color = "red"
|
694
|
+
product.restore_color!
|
695
|
+
expect(product.color).to be_nil
|
696
|
+
end
|
697
|
+
|
698
|
+
context "persisted" do
|
699
|
+
it "restores the attribute" do
|
700
|
+
green = product.color = "green"
|
701
|
+
product.save!
|
702
|
+
product.color = "red"
|
703
|
+
product.restore_color!
|
704
|
+
expect(product.color).to eq(green)
|
705
|
+
end
|
706
|
+
end
|
707
|
+
end
|
708
|
+
end
|
709
|
+
end
|