active_attr 0.13.0 → 0.15.2

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.

Potentially problematic release.


This version of active_attr might be problematic. Click here for more details.

Files changed (45) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yaml +180 -0
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +34 -0
  5. data/Gemfile +6 -8
  6. data/README.md +145 -117
  7. data/active_attr.gemspec +14 -7
  8. data/gemfiles/rails_3_0.gemfile +0 -4
  9. data/gemfiles/rails_3_1.gemfile +1 -8
  10. data/gemfiles/rails_3_2.gemfile +1 -12
  11. data/gemfiles/rails_4_0.gemfile +1 -1
  12. data/gemfiles/rails_4_1.gemfile +1 -1
  13. data/gemfiles/rails_4_2.gemfile +1 -1
  14. data/gemfiles/rails_5_0.gemfile +2 -1
  15. data/gemfiles/rails_5_1.gemfile +2 -1
  16. data/gemfiles/rails_5_2.gemfile +10 -0
  17. data/gemfiles/rails_6_0.gemfile +10 -0
  18. data/gemfiles/rails_head.gemfile +3 -2
  19. data/lib/active_attr.rb +1 -1
  20. data/lib/active_attr/attributes.rb +63 -9
  21. data/lib/active_attr/chainable_initialization.rb +2 -6
  22. data/lib/active_attr/logger.rb +1 -1
  23. data/lib/active_attr/mass_assignment.rb +2 -2
  24. data/lib/active_attr/matchers/have_attribute_matcher.rb +1 -1
  25. data/lib/active_attr/model.rb +1 -0
  26. data/lib/active_attr/query_attributes.rb +1 -1
  27. data/lib/active_attr/railtie.rb +5 -0
  28. data/lib/active_attr/typecasting/big_decimal_typecaster.rb +3 -0
  29. data/lib/active_attr/typecasting/boolean_typecaster.rb +8 -0
  30. data/lib/active_attr/typecasting/float_typecaster.rb +3 -1
  31. data/lib/active_attr/typecasting/integer_typecaster.rb +3 -1
  32. data/lib/active_attr/version.rb +1 -1
  33. data/spec/functional/active_attr/attributes_spec.rb +8 -40
  34. data/spec/functional/active_attr/mass_assignment_spec.rb +2 -2
  35. data/spec/functional/active_attr/matchers/have_attribute_matcher_spec.rb +48 -0
  36. data/spec/functional/active_attr/model_spec.rb +47 -0
  37. data/spec/unit/active_attr/attribute_defaults_spec.rb +8 -0
  38. data/spec/unit/active_attr/attributes_spec.rb +91 -0
  39. data/spec/unit/active_attr/typecasting/big_decimal_typecaster_spec.rb +11 -3
  40. data/spec/unit/active_attr/typecasting/boolean_typecaster_spec.rb +3 -3
  41. data/spec/unit/active_attr/typecasting/float_typecaster_spec.rb +10 -2
  42. data/spec/unit/active_attr/typecasting/integer_typecaster_spec.rb +10 -2
  43. metadata +48 -33
  44. data/.ruby-version +0 -1
  45. data/.travis.yml +0 -94
@@ -1,3 +1,5 @@
1
+ require "active_support/core_ext/object/blank"
2
+
1
3
  module ActiveAttr
2
4
  module Typecasting
3
5
  # Typecasts an Object to an Integer
@@ -21,7 +23,7 @@ module ActiveAttr
21
23
  #
22
24
  # @since 0.5.0
23
25
  def call(value)
24
- value.to_i if value.respond_to? :to_i
26
+ value.to_i if value.present? && value.respond_to?(:to_i)
25
27
  rescue FloatDomainError
26
28
  end
27
29
  end
@@ -1,5 +1,5 @@
1
1
  module ActiveAttr
2
2
  # Complete version string
3
3
  # @since 0.1.0
4
- VERSION = "0.13.0"
4
+ VERSION = "0.15.2"
5
5
  end
@@ -1,7 +1,7 @@
1
1
  require "spec_helper"
2
2
  require "active_attr/attributes"
3
3
  require "active_model"
4
- require "factory_girl"
4
+ require "factory_bot"
5
5
 
6
6
  module ActiveAttr
7
7
  describe Attributes do
@@ -149,22 +149,22 @@ module ActiveAttr
149
149
  end
150
150
  end
151
151
 
152
- context "building with FactoryGirl" do
153
- subject(:model) { FactoryGirl.build(:person) }
152
+ context "building with FactoryBot" do
153
+ subject(:model) { FactoryBot.build(:person) }
154
154
 
155
155
  before do
156
156
  Object.const_set("Person", model_class)
157
157
 
158
- FactoryGirl.define do
158
+ FactoryBot.define do
159
159
  factory :person, :class => :person do
160
- first_name "Chris"
161
- last_name "Griego"
160
+ first_name { "Chris" }
161
+ last_name { "Griego" }
162
162
  end
163
163
  end
164
164
  end
165
165
 
166
166
  after do
167
- FactoryGirl.factories.clear
167
+ FactoryBot.factories.clear
168
168
  Object.send :remove_const, "Person"
169
169
  end
170
170
 
@@ -201,28 +201,6 @@ module ActiveAttr
201
201
  end
202
202
  end
203
203
 
204
- shared_examples "a whitelisted attribute" do
205
- it ".dangerous_attribute? is falsey" do
206
- model_class.dangerous_attribute?(attribute_name).should be_falsey
207
- end
208
-
209
- it ".attribute does not raise" do
210
- expect { model_class.attribute(attribute_name) }.not_to raise_error
211
- end
212
-
213
- it ".attribute! does not raise" do
214
- expect { model_class.attribute!(attribute_name) }.not_to raise_error
215
- end
216
-
217
- it "can be set and get" do
218
- model_class.attribute attribute_name
219
- model = model_class.new
220
- value = double
221
- model.send "#{attribute_name}=", value
222
- model.send(attribute_name).should equal value
223
- end
224
- end
225
-
226
204
  shared_examples "defining dangerous attributes" do
227
205
  context "an attribute that conflicts with #{described_class}" do
228
206
  let(:attribute_name) { :write_attribute }
@@ -258,16 +236,6 @@ module ActiveAttr
258
236
  let(:attribute_name) { :my_less_proper_missing_method }
259
237
  include_examples "a dangerous attribute"
260
238
  end
261
-
262
- context "an :id attribute" do
263
- let(:attribute_name) { :id }
264
- include_examples "a whitelisted attribute"
265
- end
266
-
267
- context "a :type attribute" do
268
- let(:attribute_name) { :type }
269
- include_examples "a whitelisted attribute"
270
- end
271
239
  end
272
240
 
273
241
  let :dangerous_model_class do
@@ -284,7 +252,7 @@ module ActiveAttr
284
252
  end
285
253
 
286
254
  def respond_to?(method_name, include_private=false)
287
- super || method_name.to_s == "my_less_proper_missing_method" || (RUBY_VERSION < "1.9" && respond_to_missing?(method_name, include_private))
255
+ super || method_name.to_s == "my_less_proper_missing_method"
288
256
  end
289
257
  end
290
258
  end
@@ -40,7 +40,7 @@ module ActiveAttr
40
40
  end
41
41
  end
42
42
 
43
- context "white listing attributes" do
43
+ context "allowing attributes" do
44
44
  before do
45
45
  model_class.class_eval do
46
46
  attr_accessible :first_name, :last_name, :name
@@ -53,7 +53,7 @@ module ActiveAttr
53
53
  describe "#initialize", :initialize, :secure_mass_assignment_method, :secure_mass_assignment_method_with_options
54
54
  end
55
55
 
56
- context "black listing attributes" do
56
+ context "denying attributes" do
57
57
  before do
58
58
  model_class.class_eval do
59
59
  attr_protected :age
@@ -14,6 +14,14 @@ module ActiveAttr
14
14
  include AttributeDefaults
15
15
  include TypecastedAttributes
16
16
 
17
+ def john
18
+ "John"
19
+ end
20
+
21
+ def doe
22
+ "Doe"
23
+ end
24
+
17
25
  def self.name
18
26
  "Person"
19
27
  end
@@ -167,6 +175,25 @@ module ActiveAttr
167
175
  end
168
176
  end
169
177
 
178
+ context "a class with the attribute and a different default (lazy evaluation)" do
179
+ before { model_class.attribute :first_name, :default => lambda { doe } }
180
+
181
+ describe "#matches?" do
182
+ it { matcher.matches?(model_class).should == false }
183
+ end
184
+
185
+ describe "#failure_message" do
186
+ before { matcher.matches?(model_class) }
187
+
188
+ it do
189
+ matcher.failure_message.should match Regexp.new <<-MESSAGE.strip_heredoc.chomp, Regexp::MULTILINE
190
+ expected: attribute :first_name, :default => "John"
191
+ got: attribute :first_name, :default => #<Proc.+?>
192
+ MESSAGE
193
+ end
194
+ end
195
+ end
196
+
170
197
  context "a class with the attribute and the right default" do
171
198
  before { model_class.attribute :first_name, :default => "John" }
172
199
 
@@ -187,6 +214,27 @@ module ActiveAttr
187
214
  end
188
215
  end
189
216
  end
217
+
218
+ context "a class with the attribute and the right default (lazy evaluation)" do
219
+ before { model_class.attribute :first_name, :default => lambda { john } }
220
+
221
+ describe "#matches?" do
222
+ it { matcher.matches?(model_class).should == true }
223
+ end
224
+
225
+ [:negative_failure_message, :failure_message_when_negated].each do |method|
226
+ describe "##{method}" do
227
+ before { matcher.matches?(model_class) }
228
+
229
+ it do
230
+ matcher.send(method).should match Regexp.new <<-MESSAGE.strip_heredoc.chomp, Regexp::MULTILINE
231
+ expected not: attribute :first_name, :default => "John"
232
+ got: attribute :first_name, :default => #<Proc.+?>
233
+ MESSAGE
234
+ end
235
+ end
236
+ end
237
+ end
190
238
  end
191
239
 
192
240
  context "a matcher with a default value of false" do
@@ -120,5 +120,52 @@ module ActiveAttr
120
120
  end.end_date.should == Date.today
121
121
  end
122
122
  end
123
+
124
+ context "when define validation callbacks" do
125
+ let :model_class do
126
+ Class.new do
127
+ include Model
128
+
129
+ attribute :name
130
+ attribute :status
131
+
132
+ validates :name, :presence => true, :length => {:maximum => 6}
133
+
134
+ before_validation :remove_whitespaces
135
+ after_validation :set_status
136
+
137
+ def self.name
138
+ "Person"
139
+ end
140
+
141
+ private
142
+
143
+ def remove_whitespaces
144
+ name.strip!
145
+ end
146
+
147
+ def set_status
148
+ self.status = errors.empty?
149
+ end
150
+ end
151
+ end
152
+
153
+ it "can call before_validation" do
154
+ person = model_class.new(:name => " bob ")
155
+
156
+ expect(person.valid?).to be(true)
157
+ expect(person.name).to eq("bob")
158
+ end
159
+
160
+ it "can call after_validation" do
161
+ person = model_class.new(:name => "")
162
+ expect(person.valid?).to be(false)
163
+ expect(person.status).to be(false)
164
+
165
+ person = model_class.new(:name => "alice")
166
+ expect(person.valid?).to be(true)
167
+ expect(person.status).to be(true)
168
+ end
169
+ end
123
170
  end
124
171
  end
@@ -4,14 +4,18 @@ require "active_attr/attribute_defaults"
4
4
  module ActiveAttr
5
5
  describe AttributeDefaults do
6
6
  subject(:model) { model_class.new }
7
+ let(:non_duplicable) { double("non-duplicable", :duplicable? => false) }
7
8
 
8
9
  let :model_class do
10
+ non_duplicable_default = non_duplicable
11
+
9
12
  Class.new do
10
13
  include InitializationVerifier
11
14
  include AttributeDefaults
12
15
  attribute :first_name, :default => "John"
13
16
  attribute :age, :default => nil
14
17
  attribute :created_at, :default => lambda { Time.now }
18
+ attribute :non_duplicable, :default => non_duplicable_default
15
19
  end
16
20
  end
17
21
 
@@ -32,6 +36,10 @@ module ActiveAttr
32
36
  it "includes declared dynamic attribute defaults" do
33
37
  attribute_defaults["created_at"].should be_a_kind_of Time
34
38
  end
39
+
40
+ it "includes non-duplicable attribute defaults" do
41
+ attribute_defaults["non_duplicable"].should == non_duplicable
42
+ end
35
43
  end
36
44
 
37
45
  describe "#initialize" do
@@ -9,6 +9,7 @@ module ActiveAttr
9
9
  let :model_class do
10
10
  Class.new do
11
11
  include Attributes
12
+
12
13
  attribute :first_name
13
14
  attribute :last_name
14
15
  attribute :amount
@@ -133,6 +134,11 @@ module ActiveAttr
133
134
  model.should_receive(:attribute=).with("first_name", "Ben")
134
135
  model.first_name = "Ben"
135
136
  end
137
+
138
+ it "returns the new attribute definition" do
139
+ result = attributeless.attribute! :first_name
140
+ result.should == AttributeDefinition.new(:first_name)
141
+ end
136
142
  end
137
143
 
138
144
  describe ".attributes" do
@@ -151,6 +157,38 @@ module ActiveAttr
151
157
  end
152
158
  end
153
159
 
160
+ describe "#{described_class}.filter_attributes" do
161
+ after { described_class.send(:remove_instance_variable, "@filter_attributes") }
162
+
163
+ it "defaults to an empty array" do
164
+ described_class.filter_attributes.should == []
165
+ end
166
+
167
+ it "can be mutated" do
168
+ described_class.filter_attributes << :password
169
+ described_class.filter_attributes.should == [:password]
170
+ end
171
+
172
+ it "can be assigned" do
173
+ described_class.filter_attributes += [:password]
174
+ described_class.filter_attributes.should == [:password]
175
+ end
176
+
177
+ it "initiailizes new classes using the module config" do
178
+ described_class.filter_attributes += [:password]
179
+ model_class.filter_attributes.should == [:password]
180
+ end
181
+
182
+ it "subclasses should inherit from the parent class" do
183
+ model_class.filter_attributes = [:password]
184
+ Class.new(model_class).filter_attributes.should == [:password]
185
+ end
186
+
187
+ it "cannot be overridden on instances" do
188
+ model_class.new.should_not respond_to(:filter_attributes=)
189
+ end
190
+ end
191
+
154
192
  describe ".inspect" do
155
193
  it "renders the class name" do
156
194
  model_class.inspect.should match(/^Foo\(.*\)$/)
@@ -214,6 +252,59 @@ module ActiveAttr
214
252
  model.inspect.should == %{#<Foo amount: nil, first_name: "Ben", last_name: "#{last_name}">}
215
253
  end
216
254
 
255
+ context "filtering attributes with a Symbol" do
256
+ before { model_class.filter_attributes = [:amount] }
257
+
258
+ it "does not filter an attribute with a nil value" do
259
+ model.inspect.should == %{#<Foo amount: nil, first_name: "Ben", last_name: "#{last_name}">}
260
+ end
261
+
262
+ it "filters filtered attributes that have a value" do
263
+ model.amount = 100
264
+ model.inspect.should == %{#<Foo amount: [FILTERED], first_name: "Ben", last_name: "#{last_name}">}
265
+ end
266
+ end
267
+
268
+ context "filtering attributes with a String" do
269
+ before { model_class.filter_attributes = ["amount"] }
270
+
271
+ it "does not filter an attribute with a nil value" do
272
+ model.inspect.should == %{#<Foo amount: nil, first_name: "Ben", last_name: "#{last_name}">}
273
+ end
274
+
275
+ it "filters filtered attributes that have a value" do
276
+ model.amount = 100
277
+ model.inspect.should == %{#<Foo amount: [FILTERED], first_name: "Ben", last_name: "#{last_name}">}
278
+ end
279
+ end
280
+
281
+ context "filtering attributes with a partial attribute name" do
282
+ before { model_class.filter_attributes = [:name] }
283
+
284
+ it "filters filtered attributes that have a value and whose attribute contains the filtered attribute name" do
285
+ model.inspect.should == %{#<Foo amount: nil, first_name: [FILTERED], last_name: [FILTERED]>}
286
+ end
287
+ end
288
+
289
+ context "filtering attributes with a Regexp" do
290
+ before { model_class.filter_attributes = [/name/i] }
291
+
292
+ it "filters filtered attributes that have a value and whose attribute matches the Regexp" do
293
+ model.inspect.should == %{#<Foo amount: nil, first_name: [FILTERED], last_name: [FILTERED]>}
294
+ end
295
+ end
296
+
297
+ context "filtering attributes with a Proc with 2 arguments" do
298
+ before do
299
+ model.amount = 100
300
+ model_class.filter_attributes = [->(key, value) { value.reverse! if key =~ /name/ }]
301
+ end
302
+
303
+ it "filters filtered attributes that have a value by mutating the attributes" do
304
+ model.inspect.should == %{#<Foo amount: 100, first_name: "neB", last_name: "iksewoP">}
305
+ end
306
+ end
307
+
217
308
  it "doesn't format the inspection string for attributes if the model does not have any" do
218
309
  attributeless.new.inspect.should == %{#<Foo>}
219
310
  end
@@ -12,18 +12,26 @@ module ActiveAttr
12
12
  typecaster.call(value).should equal value
13
13
  end
14
14
 
15
- it "casts nil to a zero BigDecimal" do
16
- typecaster.call(nil).should eql BigDecimal("0.0")
15
+ it "casts nil to nil" do
16
+ typecaster.call(nil).should eql nil
17
17
  end
18
18
 
19
19
  it "casts a numeric String to a BigDecimal" do
20
20
  typecaster.call("2").should eql BigDecimal("2.0")
21
21
  end
22
22
 
23
- it "casts a alpha String to a zero BigDecimal" do
23
+ it "casts an empty String to nil" do
24
+ typecaster.call("").should eql nil
25
+ end
26
+
27
+ it "casts an alpha String to a zero BigDecimal" do
24
28
  typecaster.call("bob").should eql BigDecimal("0.0")
25
29
  end
26
30
 
31
+ it "casts an alpha string coercable object to a zero BigDecimal" do
32
+ typecaster.call(double(to_s: "bob")).should eql BigDecimal("0.0")
33
+ end
34
+
27
35
  it "casts a Rational to a BigDecimal" do
28
36
  typecaster.call(Rational(1, 2)).should eql BigDecimal("0.5")
29
37
  end
@@ -16,8 +16,8 @@ module ActiveAttr
16
16
  typecaster.call(false).should equal false
17
17
  end
18
18
 
19
- it "casts nil to false" do
20
- typecaster.call(nil).should equal false
19
+ it "casts nil to nil" do
20
+ typecaster.call(nil).should equal nil
21
21
  end
22
22
 
23
23
  it "casts an Object to true" do
@@ -26,7 +26,7 @@ module ActiveAttr
26
26
 
27
27
  context "when the value is a String" do
28
28
  it "casts an empty String to false" do
29
- typecaster.call("").should equal false
29
+ typecaster.call("").should equal nil
30
30
  end
31
31
 
32
32
  it "casts a non-empty String to true" do