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.
@@ -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