hstore_accessor_moi_solutions 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,35 @@
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_moi_solutions"
8
+ spec.version = HstoreAccessor::VERSION
9
+ spec.authors = ["César Bolaños"]
10
+ spec.email = ["cesar.bolanos.andino@gmail.com"]
11
+ spec.description = "Adds typed hstore backed fields to an ActiveRecord model (FORK)."
12
+ spec.summary = "Adds typed hstore backed fields to an ActiveRecord model (FORK)."
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
+ spec.add_development_dependency "shoulda-matchers"
33
+
34
+ spec.post_install_message = "Please note that the `array` and `hash` types are no longer supported in version 1.0.0"
35
+ end
@@ -0,0 +1,26 @@
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 and ::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
+ require "hstore_accessor/serialization"
15
+ require "hstore_accessor/macro"
16
+ require "bigdecimal"
17
+
18
+ module HstoreAccessor
19
+ extend ActiveSupport::Concern
20
+ include Serialization
21
+ include Macro
22
+ end
23
+
24
+ ActiveSupport.on_load(:active_record) do
25
+ ActiveRecord::Base.send(:include, HstoreAccessor)
26
+ 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,715 @@
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
+
75
+ context "instance method" do
76
+ subject { Product.new }
77
+ it { is_expected.to delegate_method(:hstore_metadata_for_options).to(:class) }
78
+ it { is_expected.to delegate_method(:hstore_metadata_for_data).to(:class) }
79
+ end
80
+ end
81
+
82
+ context "nil values" do
83
+ let!(:timestamp) { Time.now }
84
+ let!(:datestamp) { Date.today }
85
+ let(:product) { Product.new }
86
+ 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")) }
87
+
88
+ FIELDS.keys.each do |field|
89
+ it "responds with nil when #{field} is not set" do
90
+ expect(product.send(field)).to be_nil
91
+ end
92
+
93
+ it "responds with nil when #{field} is set back to nil after being set initially" do
94
+ persisted_product.send("#{field}=", nil)
95
+ expect(persisted_product.send(field)).to be_nil
96
+ end
97
+ end
98
+ end
99
+
100
+ describe "predicate methods" do
101
+ let!(:product) { Product.new }
102
+
103
+ it "exist for each field" do
104
+ FIELDS.keys.each do |field|
105
+ expect(product).to respond_to "#{field}?"
106
+ end
107
+ end
108
+
109
+ it "uses 'present?' to determine return value" do
110
+ stub = double(present?: :result_of_present)
111
+ expect(stub).to receive(:present?)
112
+ allow(product).to receive_messages(color: stub)
113
+ expect(product.color?).to eq(:result_of_present)
114
+ end
115
+
116
+ context "boolean fields" do
117
+ it "return the state for true boolean fields" do
118
+ product.popular = true
119
+ product.save
120
+ product.reload
121
+ expect(product.popular?).to be true
122
+ end
123
+
124
+ it "return the state for false boolean fields" do
125
+ product.popular = false
126
+ product.save
127
+ product.reload
128
+ expect(product.popular?).to be false
129
+ end
130
+
131
+ it "return true for boolean field set via hash using real boolean" do
132
+ product.options = { "popular" => true }
133
+ expect(product.popular?).to be true
134
+ end
135
+
136
+ it "return false for boolean field set via hash using real boolean" do
137
+ product.options = { "popular" => false }
138
+ expect(product.popular?).to be false
139
+ end
140
+
141
+ it "return true for boolean field set via hash using string" do
142
+ product.options = { "popular" => "true" }
143
+ expect(product.popular?).to be true
144
+ end
145
+
146
+ it "return false for boolean field set via hash using string" do
147
+ product.options = { "popular" => "false" }
148
+ expect(product.popular?).to be false
149
+ end
150
+ end
151
+ end
152
+
153
+ describe "scopes" do
154
+ let!(:timestamp) { Time.now }
155
+ let!(:datestamp) { Date.today }
156
+ 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")) }
157
+ 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")) }
158
+ 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")) }
159
+
160
+ context "ambiguous column names" do
161
+ let!(:product_category) { ProductCategory.create!(name: "widget", likes: 2) }
162
+
163
+ before do
164
+ Product.all.to_a.each do |product|
165
+ product_category.products << product
166
+ end
167
+ end
168
+
169
+ context "eq query" do
170
+ let!(:query) { Product.all.joins(:product_category).merge(ProductCategory.with_name("widget")).with_name("widget") }
171
+
172
+ it "qualifies the table name to prevent ambiguous column name references" do
173
+ expect { query.to_a }.to_not raise_error
174
+ end
175
+ end
176
+
177
+ context "query" do
178
+ let!(:query) { Product.all.joins(:product_category).merge(ProductCategory.likes_lt(4)).likes_lt(4) }
179
+
180
+ it "qualifies the table name to prevent ambiguous column name references" do
181
+ expect { query.to_a }.to_not raise_error
182
+ end
183
+ end
184
+ end
185
+
186
+ context "for string fields support" do
187
+ it "equality" do
188
+ expect(Product.with_color("orange").to_a).to eq [product_b]
189
+ end
190
+ end
191
+
192
+ context "for integer fields support" do
193
+ it "less than" do
194
+ expect(Product.price_lt(20).to_a).to eq [product_a]
195
+ end
196
+
197
+ it "less than or equal" do
198
+ expect(Product.price_lte(20).to_a).to eq [product_a, product_b]
199
+ end
200
+
201
+ it "equality" do
202
+ expect(Product.price_eq(10).to_a).to eq [product_a]
203
+ end
204
+
205
+ it "greater than or equal" do
206
+ expect(Product.price_gte(20).to_a).to eq [product_b, product_c]
207
+ end
208
+
209
+ it "greater than" do
210
+ expect(Product.price_gt(20).to_a).to eq [product_c]
211
+ end
212
+ end
213
+
214
+ context "for float fields support" do
215
+ it "less than" do
216
+ expect(Product.weight_lt(20.0).to_a).to eq [product_a]
217
+ end
218
+
219
+ it "less than or equal" do
220
+ expect(Product.weight_lte(20.2).to_a).to eq [product_a, product_b]
221
+ end
222
+
223
+ it "equality" do
224
+ expect(Product.weight_eq(10.1).to_a).to eq [product_a]
225
+ end
226
+
227
+ it "greater than or equal" do
228
+ expect(Product.weight_gte(20.2).to_a).to eq [product_b, product_c]
229
+ end
230
+
231
+ it "greater than" do
232
+ expect(Product.weight_gt(20.5).to_a).to eq [product_c]
233
+ end
234
+ end
235
+
236
+ context "for decimal fields support" do
237
+ it "less than" do
238
+ expect(Product.miles_lt(BigDecimal.new("10.55555")).to_a).to eq [product_a]
239
+ end
240
+
241
+ it "less than or equal" do
242
+ expect(Product.miles_lte(BigDecimal.new("20.213379001")).to_a).to eq [product_a, product_b]
243
+ end
244
+
245
+ it "equality" do
246
+ expect(Product.miles_eq(BigDecimal.new("10.113379001")).to_a).to eq [product_a]
247
+ end
248
+
249
+ it "greater than or equal" do
250
+ expect(Product.miles_gte(BigDecimal.new("20.213379001")).to_a).to eq [product_b, product_c]
251
+ end
252
+
253
+ it "greater than" do
254
+ expect(Product.miles_gt(BigDecimal.new("20.555555")).to_a).to eq [product_c]
255
+ end
256
+ end
257
+
258
+ context "for datetime fields support" do
259
+ it "before" do
260
+ expect(Product.build_timestamp_before(timestamp)).to eq [product_a, product_b]
261
+ end
262
+
263
+ it "equality" do
264
+ expect(Product.build_timestamp_eq(timestamp)).to eq [product_c]
265
+ end
266
+
267
+ it "after" do
268
+ expect(Product.build_timestamp_after(timestamp - 6.days)).to eq [product_b, product_c]
269
+ end
270
+ end
271
+
272
+ context "for date fields support" do
273
+ it "before" do
274
+ expect(Product.released_at_before(datestamp)).to eq [product_a, product_b]
275
+ end
276
+
277
+ it "equality" do
278
+ expect(Product.released_at_eq(datestamp)).to eq [product_c]
279
+ end
280
+
281
+ it "after" do
282
+ expect(Product.released_at_after(datestamp - 6.days)).to eq [product_b, product_c]
283
+ end
284
+ end
285
+
286
+ context "for boolean field support" do
287
+ it "true" do
288
+ expect(Product.is_popular).to eq [product_a, product_c]
289
+ end
290
+
291
+ it "false" do
292
+ expect(Product.not_popular).to eq [product_b]
293
+ end
294
+ end
295
+ end
296
+
297
+ describe "#type_for_attribute" do
298
+ if ::ActiveRecord::VERSION::STRING.to_f >= 4.2
299
+ subject { SuperProduct }
300
+
301
+ def self.it_returns_the_type_for_the_attribute(type, attribute_name, active_record_type)
302
+ context "#{type}" do
303
+ it "returns the type for the column" do
304
+ expect(subject.type_for_attribute(attribute_name.to_s)).to eq(active_record_type.new)
305
+ end
306
+ end
307
+ end
308
+
309
+ it_returns_the_type_for_the_attribute "default behavior", :string_type, ActiveRecord::Type::String
310
+ it_returns_the_type_for_the_attribute :string, :color, ActiveRecord::Type::String
311
+ it_returns_the_type_for_the_attribute :integer, :price, ActiveRecord::Type::Integer
312
+ it_returns_the_type_for_the_attribute :float, :weight, ActiveRecord::Type::Float
313
+ it_returns_the_type_for_the_attribute :datetime, :build_timestamp, ActiveRecord::Type::DateTime
314
+ it_returns_the_type_for_the_attribute :date, :released_at, ActiveRecord::Type::Date
315
+ it_returns_the_type_for_the_attribute :boolean, :published, ActiveRecord::Type::Boolean
316
+ else
317
+ subject { Product }
318
+
319
+ it "is not defined" do
320
+ expect(subject).to_not respond_to(:type_for_attribute)
321
+ end
322
+ end
323
+ end
324
+
325
+ describe "#column_for_attribute" do
326
+ if ActiveRecord::VERSION::STRING.to_f >= 4.2
327
+
328
+ def self.it_returns_the_properly_typed_column(type, attribute_name, cast_type_class)
329
+ context "#{type}" do
330
+ subject { SuperProduct.column_for_attribute(attribute_name) }
331
+ it "returns a column with a #{type} cast type" do
332
+ expect(subject).to be_a(ActiveRecord::ConnectionAdapters::Column)
333
+ expect(subject.cast_type).to eq(cast_type_class.new)
334
+ end
335
+ end
336
+ end
337
+
338
+ it_returns_the_properly_typed_column :string, :color, ActiveRecord::Type::String
339
+ it_returns_the_properly_typed_column :integer, :price, ActiveRecord::Type::Integer
340
+ it_returns_the_properly_typed_column :boolean, :published, ActiveRecord::Type::Boolean
341
+ it_returns_the_properly_typed_column :float, :weight, ActiveRecord::Type::Float
342
+ it_returns_the_properly_typed_column :datetime, :build_timestamp, ActiveRecord::Type::DateTime
343
+ it_returns_the_properly_typed_column :date, :released_at, ActiveRecord::Type::Date
344
+ it_returns_the_properly_typed_column :decimal, :miles, ActiveRecord::Type::Decimal
345
+ else
346
+ def self.it_returns_the_properly_typed_column(hstore_type, attribute_name, active_record_type)
347
+ context "#{hstore_type}" do
348
+ subject { SuperProduct.new.column_for_attribute(attribute_name) }
349
+ it "returns a column with a #{hstore_type} cast type" do
350
+ expect(subject).to be_a(ActiveRecord::ConnectionAdapters::Column)
351
+ expect(subject.type).to eq(active_record_type)
352
+ end
353
+ end
354
+ end
355
+
356
+ it_returns_the_properly_typed_column :string, :color, :string
357
+ it_returns_the_properly_typed_column :integer, :price, :integer
358
+ it_returns_the_properly_typed_column :boolean, :published, :boolean
359
+ it_returns_the_properly_typed_column :float, :weight, :float
360
+ it_returns_the_properly_typed_column :time, :build_timestamp, :datetime
361
+ it_returns_the_properly_typed_column :date, :released_at, :date
362
+ it_returns_the_properly_typed_column :decimal, :miles, :decimal
363
+ end
364
+ end
365
+
366
+ context "when assigning values it" do
367
+ let(:product) { Product.new }
368
+
369
+ it "correctly stores string values" do
370
+ product.color = "blue"
371
+ product.save
372
+ product.reload
373
+ expect(product.color).to eq "blue"
374
+ end
375
+
376
+ it "allows access to bulk set values via string before saving" do
377
+ product.options = {
378
+ "color" => "blue",
379
+ "price" => 120
380
+ }
381
+ expect(product.color).to eq "blue"
382
+ expect(product.price).to eq 120
383
+ end
384
+
385
+ it "allows access to bulk set values via :symbols before saving" do
386
+ product.options = {
387
+ color: "blue",
388
+ price: 120
389
+ }
390
+ expect(product.color).to eq "blue"
391
+ expect(product.price).to eq 120
392
+ end
393
+
394
+ it "correctly stores integer values" do
395
+ product.price = 468
396
+ product.save
397
+ product.reload
398
+ expect(product.price).to eq 468
399
+ end
400
+
401
+ it "correctly stores float values" do
402
+ product.weight = 93.45
403
+ product.save
404
+ product.reload
405
+ expect(product.weight).to eq 93.45
406
+ end
407
+
408
+ context "multipart values" do
409
+ it "stores multipart dates correctly" do
410
+ product.update_attributes!(
411
+ "released_at(1i)" => "2014",
412
+ "released_at(2i)" => "04",
413
+ "released_at(3i)" => "14"
414
+ )
415
+ product.reload
416
+ expect(product.released_at).to eq(Date.new(2014, 4, 14))
417
+ end
418
+ end
419
+
420
+ context "time values" do
421
+ let(:chicago_timezone) { ActiveSupport::TimeZone["America/Chicago"] }
422
+ let(:new_york_timezone) { ActiveSupport::TimeZone["America/New_York"] }
423
+
424
+ it "correctly stores value" do
425
+ timestamp = Time.now - 10.days
426
+ product.build_timestamp = timestamp
427
+ product.save!
428
+ product.reload
429
+ expect(product.build_timestamp.to_i).to eq timestamp.to_i
430
+ end
431
+
432
+ it "stores the value in utc" do
433
+ timestamp = Time.now.in_time_zone(chicago_timezone) - 10.days
434
+ product.build_timestamp = timestamp
435
+ product.save!
436
+ product.reload
437
+ expect(product.options["build_timestamp"].to_i).to eq timestamp.utc.to_i
438
+ end
439
+
440
+ it "returns the time value in the current time zone" do
441
+ timestamp = Time.now.in_time_zone(chicago_timezone) - 10.days
442
+ product.build_timestamp = timestamp
443
+ product.save!
444
+ product.reload
445
+ Time.use_zone(new_york_timezone) do
446
+ expect(product.build_timestamp.to_s).to eq timestamp.in_time_zone(new_york_timezone).to_s
447
+ end
448
+ end
449
+ end
450
+
451
+ it "correctly stores date values" do
452
+ datestamp = Date.today - 9.days
453
+ product.released_at = datestamp
454
+ product.save
455
+ product.reload
456
+ expect(product.released_at.to_s).to eq datestamp.to_s
457
+ expect(product.released_at).to eq datestamp
458
+ end
459
+
460
+ it "correctly stores decimal values" do
461
+ decimal = BigDecimal.new("9.13370009001")
462
+ product.miles = decimal
463
+ product.save
464
+ product.reload
465
+ expect(product.miles.to_s).to eq decimal.to_s
466
+ expect(product.miles).to eq decimal
467
+ end
468
+
469
+ context "correctly stores boolean values" do
470
+ it "when string 'true' is passed" do
471
+ product.popular = "true"
472
+ product.save
473
+ product.reload
474
+ expect(product.popular).to be true
475
+ end
476
+
477
+ it "when a real boolean is passed" do
478
+ product.popular = true
479
+ product.save
480
+ product.reload
481
+ expect(product.popular).to be true
482
+ end
483
+ end
484
+
485
+ it "setters call the _will_change! method of the store attribute" do
486
+ expect(product).to receive(:options_will_change!)
487
+ product.color = "green"
488
+ end
489
+
490
+ describe "type casting" do
491
+ it "type casts integer values" do
492
+ product.price = "468"
493
+ expect(product.price).to eq 468
494
+ end
495
+
496
+ it "type casts float values" do
497
+ product.weight = "93.45"
498
+ expect(product.weight).to eq 93.45
499
+ end
500
+
501
+ it "type casts time values" do
502
+ timestamp = Time.now - 10.days
503
+ product.build_timestamp = timestamp.to_s
504
+ expect(product.build_timestamp.to_i).to eq timestamp.to_i
505
+ end
506
+
507
+ it "type casts date values" do
508
+ datestamp = Date.today - 9.days
509
+ product.released_at = datestamp.to_s
510
+ expect(product.released_at).to eq datestamp
511
+ end
512
+
513
+ it "type casts decimal values" do
514
+ product.miles = "1.337900129339202"
515
+ expect(product.miles).to eq BigDecimal.new("1.337900129339202")
516
+ end
517
+
518
+ it "type casts boolean values" do
519
+ ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.each do |value|
520
+ product.popular = value
521
+ expect(product.popular).to be true
522
+
523
+ product.published = value
524
+ expect(product.published).to be true
525
+ end
526
+
527
+ ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.each do |value|
528
+ product.popular = value
529
+ expect(product.popular).to be false
530
+
531
+ product.published = value
532
+ expect(product.published).to be false
533
+ end
534
+ end
535
+ end
536
+
537
+ context "extended getters and setters" do
538
+ before do
539
+ class Product
540
+ alias_method :set_color, :color=
541
+ alias_method :get_color, :color
542
+
543
+ def color=(value)
544
+ super(value.upcase)
545
+ end
546
+
547
+ def color
548
+ super.try(:downcase)
549
+ end
550
+ end
551
+ end
552
+
553
+ after do
554
+ class Product
555
+ alias_method :color=, :set_color
556
+ alias_method :color, :get_color
557
+ end
558
+ end
559
+
560
+ context "setters" do
561
+ it "can be wrapped" do
562
+ product.color = "red"
563
+ expect(product.options["color"]).to eq("RED")
564
+ end
565
+ end
566
+
567
+ context "getters" do
568
+ it "can be wrapped" do
569
+ product.color = "GREEN"
570
+ expect(product.color).to eq("green")
571
+ end
572
+ end
573
+ end
574
+ end
575
+
576
+ describe "dirty tracking" do
577
+ let(:product) { Product.new }
578
+
579
+ it "<attr>_changed? should return the expected value" do
580
+ expect(product.color_changed?).to be false
581
+ product.color = "ORANGE"
582
+ expect(product.price_changed?).to be false
583
+ expect(product.color_changed?).to be true
584
+ product.save
585
+ expect(product.color_changed?).to be false
586
+ product.color = "ORANGE"
587
+ expect(product.color_changed?).to be false
588
+
589
+ expect(product.price_changed?).to be false
590
+ product.price = 100
591
+ expect(product.price_changed?).to be true
592
+ product.save
593
+ expect(product.price_changed?).to be false
594
+ product.price = "100"
595
+ expect(product.price).to be 100
596
+ expect(product.price_changed?).to be false
597
+ end
598
+
599
+ describe "#<attr>_will_change!" do
600
+ it "tells ActiveRecord the hstore attribute has changed" do
601
+ expect(product).to receive(:options_will_change!)
602
+ product.color_will_change!
603
+ end
604
+ end
605
+
606
+ describe "#<attr>_was" do
607
+ it "returns the expected value" do
608
+ product.color = "ORANGE"
609
+ product.save
610
+ product.color = "GREEN"
611
+ expect(product.color_was).to eq "ORANGE"
612
+ end
613
+
614
+ it "works when the hstore attribute is nil" do
615
+ product.options = nil
616
+ product.save
617
+ product.color = "green"
618
+ expect { product.color_was }.to_not raise_error
619
+ end
620
+ end
621
+
622
+ describe "#<attr>_change" do
623
+ it "returns the old and new values" do
624
+ product.color = "ORANGE"
625
+ product.save
626
+ product.color = "GREEN"
627
+ expect(product.color_change).to eq %w(ORANGE GREEN)
628
+ end
629
+
630
+ context "when store_key differs from key" do
631
+ it "returns the old and new values" do
632
+ product.weight = 100.01
633
+ expect(product.weight_change[1]).to eq "100.01"
634
+ end
635
+ end
636
+
637
+ context "hstore attribute was nil" do
638
+ it "returns old and new values" do
639
+ product.options = nil
640
+ product.save!
641
+ green = product.color = "green"
642
+ expect(product.color_change).to eq([nil, green])
643
+ end
644
+ end
645
+
646
+ context "other hstore attributes were persisted" do
647
+ it "returns nil" do
648
+ product.price = 5
649
+ product.save!
650
+ product.price = 6
651
+ expect(product.color_change).to be_nil
652
+ end
653
+
654
+ it "returns nil when other attributes were changed" do
655
+ product.price = 5
656
+ product.save!
657
+ product = Product.first
658
+
659
+ expect(product.price_change).to be_nil
660
+
661
+ product.color = "red"
662
+
663
+ expect(product.price_change).to be_nil
664
+ end
665
+ end
666
+
667
+ context "not persisted" do
668
+ it "returns nil when there are no changes" do
669
+ expect(product.color_change).to be_nil
670
+ end
671
+ end
672
+ end
673
+
674
+ describe "#reset_<attr>!" do
675
+ before do
676
+ allow(ActiveSupport::Deprecation).to receive(:warn)
677
+ end
678
+
679
+ if ActiveRecord::VERSION::STRING.to_f >= 4.2
680
+ it "displays a deprecation warning" do
681
+ expect(ActiveSupport::Deprecation).to receive(:warn)
682
+ product.reset_color!
683
+ end
684
+ else
685
+ it "does not display a deprecation warning" do
686
+ expect(ActiveSupport::Deprecation).to_not receive(:warn)
687
+ product.reset_color!
688
+ end
689
+ end
690
+
691
+ it "restores the attribute" do
692
+ expect(product).to receive(:restore_color!)
693
+ product.reset_color!
694
+ end
695
+ end
696
+
697
+ describe "#restore_<attr>!" do
698
+ it "restores the attribute" do
699
+ product.color = "red"
700
+ product.restore_color!
701
+ expect(product.color).to be_nil
702
+ end
703
+
704
+ context "persisted" do
705
+ it "restores the attribute" do
706
+ green = product.color = "green"
707
+ product.save!
708
+ product.color = "red"
709
+ product.restore_color!
710
+ expect(product.color).to eq(green)
711
+ end
712
+ end
713
+ end
714
+ end
715
+ end