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.
- checksums.yaml +5 -5
- data/.github/workflows/test.yaml +180 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +34 -0
- data/Gemfile +6 -8
- data/README.md +145 -117
- data/active_attr.gemspec +14 -7
- data/gemfiles/rails_3_0.gemfile +0 -4
- data/gemfiles/rails_3_1.gemfile +1 -8
- data/gemfiles/rails_3_2.gemfile +1 -12
- data/gemfiles/rails_4_0.gemfile +1 -1
- data/gemfiles/rails_4_1.gemfile +1 -1
- data/gemfiles/rails_4_2.gemfile +1 -1
- data/gemfiles/rails_5_0.gemfile +2 -1
- data/gemfiles/rails_5_1.gemfile +2 -1
- data/gemfiles/rails_5_2.gemfile +10 -0
- data/gemfiles/rails_6_0.gemfile +10 -0
- data/gemfiles/rails_head.gemfile +3 -2
- data/lib/active_attr.rb +1 -1
- data/lib/active_attr/attributes.rb +63 -9
- data/lib/active_attr/chainable_initialization.rb +2 -6
- data/lib/active_attr/logger.rb +1 -1
- data/lib/active_attr/mass_assignment.rb +2 -2
- data/lib/active_attr/matchers/have_attribute_matcher.rb +1 -1
- data/lib/active_attr/model.rb +1 -0
- data/lib/active_attr/query_attributes.rb +1 -1
- data/lib/active_attr/railtie.rb +5 -0
- data/lib/active_attr/typecasting/big_decimal_typecaster.rb +3 -0
- data/lib/active_attr/typecasting/boolean_typecaster.rb +8 -0
- data/lib/active_attr/typecasting/float_typecaster.rb +3 -1
- data/lib/active_attr/typecasting/integer_typecaster.rb +3 -1
- data/lib/active_attr/version.rb +1 -1
- data/spec/functional/active_attr/attributes_spec.rb +8 -40
- data/spec/functional/active_attr/mass_assignment_spec.rb +2 -2
- data/spec/functional/active_attr/matchers/have_attribute_matcher_spec.rb +48 -0
- data/spec/functional/active_attr/model_spec.rb +47 -0
- data/spec/unit/active_attr/attribute_defaults_spec.rb +8 -0
- data/spec/unit/active_attr/attributes_spec.rb +91 -0
- data/spec/unit/active_attr/typecasting/big_decimal_typecaster_spec.rb +11 -3
- data/spec/unit/active_attr/typecasting/boolean_typecaster_spec.rb +3 -3
- data/spec/unit/active_attr/typecasting/float_typecaster_spec.rb +10 -2
- data/spec/unit/active_attr/typecasting/integer_typecaster_spec.rb +10 -2
- metadata +48 -33
- data/.ruby-version +0 -1
- 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?
|
26
|
+
value.to_i if value.present? && value.respond_to?(:to_i)
|
25
27
|
rescue FloatDomainError
|
26
28
|
end
|
27
29
|
end
|
data/lib/active_attr/version.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
require "active_attr/attributes"
|
3
3
|
require "active_model"
|
4
|
-
require "
|
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
|
153
|
-
subject(:model) {
|
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
|
-
|
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
|
-
|
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"
|
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 "
|
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 "
|
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
|
16
|
-
typecaster.call(nil).should eql
|
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
|
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
|
20
|
-
typecaster.call(nil).should equal
|
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
|
29
|
+
typecaster.call("").should equal nil
|
30
30
|
end
|
31
31
|
|
32
32
|
it "casts a non-empty String to true" do
|