virtus 1.0.0.beta8 → 1.0.0.rc1
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.
- data/Changelog.md +28 -1
- data/LICENSE +1 -1
- data/README.md +12 -22
- data/config/flay.yml +2 -2
- data/config/flog.yml +1 -1
- data/config/mutant.yml +2 -1
- data/config/reek.yml +67 -17
- data/lib/virtus.rb +61 -5
- data/lib/virtus/attribute.rb +107 -26
- data/lib/virtus/attribute/builder.rb +42 -34
- data/lib/virtus/attribute/collection.rb +11 -6
- data/lib/virtus/attribute/embedded_value.rb +12 -7
- data/lib/virtus/attribute/hash.rb +19 -8
- data/lib/virtus/attribute_set.rb +2 -13
- data/lib/virtus/builder.rb +137 -0
- data/lib/virtus/builder/hook_context.rb +51 -0
- data/lib/virtus/class_inclusions.rb +1 -0
- data/lib/virtus/class_methods.rb +1 -5
- data/lib/virtus/configuration.rb +12 -2
- data/lib/virtus/extensions.rb +28 -32
- data/lib/virtus/instance_methods.rb +2 -2
- data/lib/virtus/model.rb +3 -1
- data/lib/virtus/module_extensions.rb +1 -1
- data/lib/virtus/value_object.rb +1 -1
- data/lib/virtus/version.rb +1 -1
- data/spec/integration/inheritance_spec.rb +42 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/unit/virtus/attribute/collection/class_methods/build_spec.rb +10 -0
- data/spec/unit/virtus/attribute/hash/class_methods/build_spec.rb +14 -0
- data/spec/unit/virtus/class_methods/finalize_spec.rb +10 -4
- data/spec/unit/virtus/model_spec.rb +35 -3
- data/spec/unit/virtus/module_spec.rb +44 -3
- data/spec/unit/virtus/value_object_spec.rb +39 -22
- data/virtus.gemspec +2 -1
- metadata +9 -6
- data/lib/virtus/module_builder.rb +0 -192
@@ -13,6 +13,7 @@ module Virtus
|
|
13
13
|
def self.included(descendant)
|
14
14
|
super
|
15
15
|
descendant.extend(ClassMethods)
|
16
|
+
descendant.extend(Extensions::AllowedWriterMethods)
|
16
17
|
descendant.class_eval { include Methods }
|
17
18
|
descendant.class_eval { include InstanceMethods }
|
18
19
|
descendant.class_eval { include InstanceMethods::Constructor }
|
data/lib/virtus/class_methods.rb
CHANGED
@@ -14,11 +14,7 @@ module Virtus
|
|
14
14
|
# @api private
|
15
15
|
def self.extended(descendant)
|
16
16
|
super
|
17
|
-
AttributeSet.create(descendant)
|
18
|
-
descendant.module_eval do
|
19
|
-
extend DescendantsTracker
|
20
|
-
include attribute_set
|
21
|
-
end
|
17
|
+
descendant.send(:include, AttributeSet.create(descendant))
|
22
18
|
end
|
23
19
|
private_class_method :extended
|
24
20
|
|
data/lib/virtus/configuration.rb
CHANGED
@@ -28,8 +28,10 @@ module Virtus
|
|
28
28
|
# @return [Configuration]
|
29
29
|
#
|
30
30
|
# @api public
|
31
|
-
def self.build(&block)
|
32
|
-
new.call(&block)
|
31
|
+
def self.build(options = {}, &block)
|
32
|
+
config = new.call(&block)
|
33
|
+
options.each { |key, value| config.public_send("#{key}=", value) }
|
34
|
+
config
|
33
35
|
end
|
34
36
|
|
35
37
|
# Initialized a configuration instance
|
@@ -77,5 +79,13 @@ module Virtus
|
|
77
79
|
@coercer
|
78
80
|
end
|
79
81
|
|
82
|
+
# @api private
|
83
|
+
def to_h
|
84
|
+
{ :coerce => coerce,
|
85
|
+
:finalize => finalize,
|
86
|
+
:strict => strict,
|
87
|
+
:configured_coercer => coercer }.freeze
|
88
|
+
end
|
89
|
+
|
80
90
|
end # class Configuration
|
81
91
|
end # module Virtus
|
data/lib/virtus/extensions.rb
CHANGED
@@ -18,9 +18,8 @@ module Virtus
|
|
18
18
|
object.instance_eval do
|
19
19
|
extend Methods
|
20
20
|
extend InstanceMethods
|
21
|
-
extend
|
21
|
+
extend AllowedWriterMethods
|
22
22
|
extend InstanceMethods::MassAssignment
|
23
|
-
extend attribute_set
|
24
23
|
end
|
25
24
|
end
|
26
25
|
private_class_method :extended
|
@@ -29,18 +28,16 @@ module Virtus
|
|
29
28
|
|
30
29
|
# @api private
|
31
30
|
def self.extended(descendant)
|
32
|
-
|
33
|
-
descendant.
|
34
|
-
extend attribute_set
|
35
|
-
end
|
31
|
+
super
|
32
|
+
descendant.extend(AttributeSet.create(descendant))
|
36
33
|
end
|
37
34
|
private_class_method :extended
|
38
35
|
|
39
|
-
# Defines an attribute on an object's class
|
36
|
+
# Defines an attribute on an object's class or instance
|
40
37
|
#
|
41
38
|
# @example
|
42
39
|
# class Book
|
43
|
-
# include Virtus
|
40
|
+
# include Virtus.model
|
44
41
|
#
|
45
42
|
# attribute :title, String
|
46
43
|
# attribute :author, String
|
@@ -52,7 +49,7 @@ module Virtus
|
|
52
49
|
# @param [Symbol] name
|
53
50
|
# the name of an attribute
|
54
51
|
#
|
55
|
-
# @param [Class] type
|
52
|
+
# @param [Class,Array,Hash,Axiom::Types::Type,String,Symbol] type
|
56
53
|
# the type class of an attribute
|
57
54
|
#
|
58
55
|
# @param [#to_hash] options
|
@@ -63,32 +60,19 @@ module Virtus
|
|
63
60
|
# @see Attribute.build
|
64
61
|
#
|
65
62
|
# @api public
|
66
|
-
def attribute(name, type =
|
63
|
+
def attribute(name, type = nil, options = {})
|
67
64
|
assert_valid_name(name)
|
68
|
-
attribute_set << Attribute.build(type,
|
65
|
+
attribute_set << Attribute.build(type, options.merge(:name => name))
|
69
66
|
self
|
70
67
|
end
|
71
68
|
|
69
|
+
# @see Virtus.default_value
|
70
|
+
#
|
72
71
|
# @api public
|
73
72
|
def values(&block)
|
74
|
-
private :attributes=
|
73
|
+
private :attributes= if instance_methods.include?(:attributes=)
|
75
74
|
yield
|
76
75
|
include(::Equalizer.new(*attribute_set.map(&:name)))
|
77
|
-
self
|
78
|
-
end
|
79
|
-
|
80
|
-
# The list of writer methods that can be mass-assigned to in #attributes=
|
81
|
-
#
|
82
|
-
# @return [Set]
|
83
|
-
#
|
84
|
-
# @api private
|
85
|
-
def allowed_writer_methods
|
86
|
-
@allowed_writer_methods ||=
|
87
|
-
begin
|
88
|
-
allowed_writer_methods = allowed_methods.grep(WRITER_METHOD_REGEXP).to_set
|
89
|
-
allowed_writer_methods -= INVALID_WRITER_METHODS
|
90
|
-
allowed_writer_methods.freeze
|
91
|
-
end
|
92
76
|
end
|
93
77
|
|
94
78
|
private
|
@@ -102,16 +86,28 @@ module Virtus
|
|
102
86
|
@attribute_set
|
103
87
|
end
|
104
88
|
|
105
|
-
|
89
|
+
end # Methods
|
90
|
+
|
91
|
+
module AllowedWriterMethods
|
92
|
+
WRITER_METHOD_REGEXP = /=\z/.freeze
|
93
|
+
INVALID_WRITER_METHODS = %w[ == != === []= attributes= ].to_set.freeze
|
94
|
+
|
95
|
+
# The list of writer methods that can be mass-assigned to in #attributes=
|
106
96
|
#
|
107
|
-
# @return [
|
97
|
+
# @return [Set]
|
108
98
|
#
|
109
99
|
# @api private
|
110
|
-
def
|
111
|
-
|
100
|
+
def allowed_writer_methods
|
101
|
+
@allowed_writer_methods ||=
|
102
|
+
begin
|
103
|
+
allowed_writer_methods = allowed_methods.grep(WRITER_METHOD_REGEXP).to_set
|
104
|
+
allowed_writer_methods -= INVALID_WRITER_METHODS
|
105
|
+
allowed_writer_methods.freeze
|
106
|
+
end
|
112
107
|
end
|
113
108
|
|
114
|
-
end #
|
109
|
+
end # AllowedWriterMethods
|
115
110
|
|
116
111
|
end # module Extensions
|
112
|
+
|
117
113
|
end # module Virtus
|
@@ -14,7 +14,7 @@ module Virtus
|
|
14
14
|
#
|
15
15
|
# @api private
|
16
16
|
def initialize(attributes = nil)
|
17
|
-
attribute_set.set(self, attributes) if attributes
|
17
|
+
self.class.attribute_set.set(self, attributes) if attributes
|
18
18
|
set_default_attributes
|
19
19
|
end
|
20
20
|
|
@@ -171,7 +171,7 @@ module Virtus
|
|
171
171
|
# @api public
|
172
172
|
def reset_attribute(attribute_name)
|
173
173
|
attribute = attribute_set[attribute_name]
|
174
|
-
|
174
|
+
attribute.set_default_value(self) if attribute
|
175
175
|
self
|
176
176
|
end
|
177
177
|
|
data/lib/virtus/model.rb
CHANGED
@@ -5,7 +5,7 @@ module Virtus
|
|
5
5
|
# @api private
|
6
6
|
def self.included(descendant)
|
7
7
|
super
|
8
|
-
descendant.send(:include, ClassInclusions)
|
8
|
+
descendant.send(:include, ClassInclusions)
|
9
9
|
end
|
10
10
|
|
11
11
|
# @api private
|
@@ -51,6 +51,7 @@ module Virtus
|
|
51
51
|
# @api private
|
52
52
|
def self.included(descendant)
|
53
53
|
super
|
54
|
+
descendant.extend(Extensions::AllowedWriterMethods)
|
54
55
|
descendant.send(:include, InstanceMethods::MassAssignment)
|
55
56
|
end
|
56
57
|
private_class_method :included
|
@@ -58,6 +59,7 @@ module Virtus
|
|
58
59
|
# @api private
|
59
60
|
def self.extended(descendant)
|
60
61
|
super
|
62
|
+
descendant.extend(Extensions::AllowedWriterMethods)
|
61
63
|
descendant.extend(InstanceMethods::MassAssignment)
|
62
64
|
end
|
63
65
|
private_class_method :included
|
data/lib/virtus/value_object.rb
CHANGED
data/lib/virtus/version.rb
CHANGED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Inheritance' do
|
4
|
+
before do
|
5
|
+
module Examples
|
6
|
+
class Base
|
7
|
+
include Virtus.model
|
8
|
+
end
|
9
|
+
|
10
|
+
class First < Base
|
11
|
+
attribute :id, Fixnum
|
12
|
+
attribute :name, String, default: ->(first, _) { "Named: #{first.id}" }
|
13
|
+
attribute :description, String
|
14
|
+
end
|
15
|
+
|
16
|
+
class Second < Base
|
17
|
+
attribute :something, String
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'inherits model from the base class' do
|
23
|
+
expect(Examples::First.attribute_set.map(&:name)).to eql([:id, :name, :description])
|
24
|
+
expect(Examples::Second.attribute_set.map(&:name)).to eql([:something])
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'sets correct attributes on the descendant classes' do
|
28
|
+
first = Examples::First.new(:id => 1, :description => 'hello world')
|
29
|
+
|
30
|
+
expect(first.id).to be(1)
|
31
|
+
expect(first.name).to eql('Named: 1')
|
32
|
+
expect(first.description).to eql('hello world')
|
33
|
+
|
34
|
+
second = Examples::Second.new
|
35
|
+
|
36
|
+
expect(second.something).to be(nil)
|
37
|
+
|
38
|
+
second.something = 'foo bar'
|
39
|
+
|
40
|
+
expect(second.something).to eql('foo bar')
|
41
|
+
end
|
42
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -19,6 +19,16 @@ describe Virtus::Attribute, '.build' do
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
+
context 'when type is Array[Virtus::Attribute::Boolean]' do
|
23
|
+
let(:type) { Array[Virtus::Attribute::Boolean] }
|
24
|
+
|
25
|
+
it_behaves_like 'a valid collection attribute instance'
|
26
|
+
|
27
|
+
it 'sets member type' do
|
28
|
+
expect(subject.type.member_type).to be(Axiom::Types::Boolean)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
22
32
|
context 'when type is Array[Float]' do
|
23
33
|
let(:type) { Array[Float] }
|
24
34
|
|
@@ -37,6 +37,20 @@ describe Virtus::Attribute::Hash, '.build' do
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
+
context 'when type is Hash[Virtus::Attribute::Hash => Virtus::Attribute::Boolean]' do
|
41
|
+
let(:type) { Hash[Virtus::Attribute::Hash => Virtus::Attribute::Boolean] }
|
42
|
+
|
43
|
+
it { should be_instance_of(Virtus::Attribute::Hash) }
|
44
|
+
|
45
|
+
it 'sets key type' do
|
46
|
+
expect(subject.type.key_type).to be(Axiom::Types::Hash)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'sets value type' do
|
50
|
+
expect(subject.type.value_type).to be(Axiom::Types::Boolean)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
40
54
|
context 'when type is Hash[Struct.new(:id) => Integer]' do
|
41
55
|
let(:type) { Hash[key_type => Integer] }
|
42
56
|
let(:key_type) { Struct.new(:id) }
|
@@ -7,29 +7,35 @@ describe Virtus, '.finalize' do
|
|
7
7
|
include Virtus.model(:finalize => false)
|
8
8
|
|
9
9
|
attribute :articles, Array['Examples::Article']
|
10
|
-
attribute :address, 'Examples::Address'
|
10
|
+
attribute :address, :'Examples::Address'
|
11
11
|
end
|
12
12
|
|
13
13
|
class Article
|
14
14
|
include Virtus.model(:finalize => false)
|
15
15
|
|
16
16
|
attribute :posts, Hash['Examples::Person' => 'Examples::Post']
|
17
|
-
attribute :person, 'Examples::Person'
|
17
|
+
attribute :person, :'Examples::Person'
|
18
18
|
end
|
19
19
|
|
20
20
|
class Post
|
21
|
-
include Virtus.model
|
21
|
+
include Virtus.model
|
22
22
|
|
23
23
|
attribute :title, String
|
24
24
|
end
|
25
25
|
|
26
26
|
class Address
|
27
|
-
include Virtus.model
|
27
|
+
include Virtus.model
|
28
28
|
|
29
29
|
attribute :street, String
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
|
+
expect(Examples::Post.attribute_set[:title]).to be_finalized
|
34
|
+
expect(Examples::Address.attribute_set[:street]).to be_finalized
|
35
|
+
|
36
|
+
expect(Virtus::Builder.pending).not_to include(Examples::Post)
|
37
|
+
expect(Virtus::Builder.pending).not_to include(Examples::Address)
|
38
|
+
|
33
39
|
Virtus.finalize
|
34
40
|
end
|
35
41
|
|
@@ -10,7 +10,7 @@ describe Virtus, '.model' do
|
|
10
10
|
|
11
11
|
share_examples_for 'a model with mass-assignment' do
|
12
12
|
let(:attributes) do
|
13
|
-
{ :name => 'Jane' }
|
13
|
+
{ :name => 'Jane', :something => nil }
|
14
14
|
end
|
15
15
|
|
16
16
|
before do
|
@@ -32,11 +32,14 @@ describe Virtus, '.model' do
|
|
32
32
|
let(:mod) { Virtus.model }
|
33
33
|
|
34
34
|
context 'with a class' do
|
35
|
-
|
35
|
+
let(:model) { Class.new }
|
36
|
+
|
37
|
+
subject { model }
|
36
38
|
|
37
39
|
before do
|
38
40
|
subject.send(:include, mod)
|
39
|
-
subject.attribute :name
|
41
|
+
subject.attribute :name, String, :default => 'Jane'
|
42
|
+
subject.attribute :something
|
40
43
|
end
|
41
44
|
|
42
45
|
it_behaves_like 'a model with constructor'
|
@@ -46,6 +49,34 @@ describe Virtus, '.model' do
|
|
46
49
|
it_behaves_like 'a model with mass-assignment' do
|
47
50
|
let(:instance) { subject.new }
|
48
51
|
end
|
52
|
+
|
53
|
+
it 'defaults to Object for attribute type' do
|
54
|
+
expect(model.attribute_set[:something].type).to be(Axiom::Types::Object)
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'with a sub-class' do
|
58
|
+
subject { Class.new(model) }
|
59
|
+
|
60
|
+
before do
|
61
|
+
subject.attribute :age, Integer
|
62
|
+
end
|
63
|
+
|
64
|
+
it_behaves_like 'a model with constructor'
|
65
|
+
|
66
|
+
it_behaves_like 'a model with strict mode turned off'
|
67
|
+
|
68
|
+
it_behaves_like 'a model with mass-assignment' do
|
69
|
+
let(:instance) { subject.new }
|
70
|
+
|
71
|
+
let(:attributes) {
|
72
|
+
{ :name => 'Jane', :something => nil, :age => 23 }
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'has its own attributes' do
|
77
|
+
expect(subject.attribute_set[:age]).to be_kind_of(Virtus::Attribute)
|
78
|
+
end
|
79
|
+
end
|
49
80
|
end
|
50
81
|
|
51
82
|
context 'with an instance' do
|
@@ -54,6 +85,7 @@ describe Virtus, '.model' do
|
|
54
85
|
before do
|
55
86
|
subject.extend(mod)
|
56
87
|
subject.attribute :name, String
|
88
|
+
subject.attribute :something
|
57
89
|
end
|
58
90
|
|
59
91
|
it_behaves_like 'a model with strict mode turned off'
|
@@ -3,8 +3,8 @@ require 'spec_helper'
|
|
3
3
|
describe Virtus, '.module' do
|
4
4
|
share_examples_for 'a valid virtus object' do
|
5
5
|
it 'reads and writes attribute' do
|
6
|
-
instance.name = '
|
7
|
-
expect(instance.name).to eql('
|
6
|
+
instance.name = 'John'
|
7
|
+
expect(instance.name).to eql('John')
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
@@ -15,6 +15,10 @@ describe Virtus, '.module' do
|
|
15
15
|
it_behaves_like 'a valid virtus object' do
|
16
16
|
let(:instance) { model.new }
|
17
17
|
end
|
18
|
+
|
19
|
+
it 'sets defaults' do
|
20
|
+
expect(instance.name).to eql('Jane')
|
21
|
+
end
|
18
22
|
end
|
19
23
|
|
20
24
|
context 'with constructor turned off' do
|
@@ -39,6 +43,16 @@ describe Virtus, '.module' do
|
|
39
43
|
expect(instance).not_to respond_to(:attributes=)
|
40
44
|
end
|
41
45
|
end
|
46
|
+
|
47
|
+
context 'with coercion turned off' do
|
48
|
+
subject { Virtus.module(:coerce => false) }
|
49
|
+
|
50
|
+
it_behaves_like 'a valid virtus object'
|
51
|
+
|
52
|
+
it 'builds non-coercible attributes' do
|
53
|
+
expect(object.send(:attribute_set)[:name]).not_to be_coercible
|
54
|
+
end
|
55
|
+
end
|
42
56
|
end
|
43
57
|
|
44
58
|
let(:mod) { Module.new }
|
@@ -47,10 +61,13 @@ describe Virtus, '.module' do
|
|
47
61
|
|
48
62
|
before do
|
49
63
|
mod.send(:include, subject)
|
50
|
-
mod.attribute :name, String
|
64
|
+
mod.attribute :name, String, :default => 'Jane'
|
65
|
+
mod.attribute :something
|
51
66
|
end
|
52
67
|
|
53
68
|
context 'with a class' do
|
69
|
+
let(:object) { model }
|
70
|
+
|
54
71
|
before do
|
55
72
|
model.send(:include, mod)
|
56
73
|
end
|
@@ -59,10 +76,16 @@ describe Virtus, '.module' do
|
|
59
76
|
expect(model.attribute_set[:name]).to be_kind_of(Virtus::Attribute)
|
60
77
|
end
|
61
78
|
|
79
|
+
it 'defaults to Object for attribute type' do
|
80
|
+
expect(model.attribute_set[:something].type).to be(Axiom::Types::Object)
|
81
|
+
end
|
82
|
+
|
62
83
|
it_behaves_like 'an object extended with virtus module'
|
63
84
|
end
|
64
85
|
|
65
86
|
context 'with a model instance' do
|
87
|
+
let(:object) { instance }
|
88
|
+
|
66
89
|
before do
|
67
90
|
instance.extend(mod)
|
68
91
|
end
|
@@ -73,4 +96,22 @@ describe Virtus, '.module' do
|
|
73
96
|
|
74
97
|
it_behaves_like 'an object extended with virtus module'
|
75
98
|
end
|
99
|
+
|
100
|
+
context 'with another module' do
|
101
|
+
let(:other) { Module.new }
|
102
|
+
|
103
|
+
let(:object) { instance }
|
104
|
+
|
105
|
+
before do
|
106
|
+
other.send(:include, mod)
|
107
|
+
model.send(:include, other)
|
108
|
+
end
|
109
|
+
|
110
|
+
it_behaves_like 'an object extended with virtus module'
|
111
|
+
|
112
|
+
it 'provides attributes for the model' do
|
113
|
+
expect(model.attribute_set[:name]).to be_kind_of(Virtus::Attribute)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
76
117
|
end
|