hstore_accessor_rails5 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "4.2.0"
6
+
7
+ group :test do
8
+ gem "pg", ">= 0.14.1"
9
+ end
10
+
11
+ gemspec :path => "../"
@@ -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,3 @@
1
+ module HstoreAccessor
2
+ VERSION = "1.0.4"
3
+ 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