active_attr 0.13.1 → 0.15.3

Sign up to get free protection for your applications and to get access to all the features.

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 +2 -1
  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 +9 -1
  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 +47 -33
  44. data/.ruby-version +0 -1
  45. data/.travis.yml +0 -111
@@ -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.1"
4
+ VERSION = "0.15.3"
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