hstore_accessor_moi_solutions 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,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