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
@@ -1,19 +1,20 @@
|
|
1
1
|
module Virtus
|
2
2
|
|
3
|
+
# Attribute placeholder used when type constant is passed as a string or symbol
|
4
|
+
#
|
3
5
|
# @private
|
4
6
|
class PendingAttribute
|
5
|
-
attr_reader :name
|
7
|
+
attr_reader :type, :options, :name
|
6
8
|
|
7
9
|
# @api private
|
8
10
|
def initialize(type, options)
|
9
|
-
@type
|
10
|
-
@
|
11
|
-
@name = options[:name]
|
11
|
+
@type, @options = type.to_s, options
|
12
|
+
@name = options[:name]
|
12
13
|
end
|
13
14
|
|
14
15
|
# @api private
|
15
16
|
def finalize
|
16
|
-
Attribute::Builder.call(determine_type,
|
17
|
+
Attribute::Builder.call(determine_type, options).finalize
|
17
18
|
end
|
18
19
|
|
19
20
|
# @api private
|
@@ -23,21 +24,24 @@ module Virtus
|
|
23
24
|
|
24
25
|
# @api private
|
25
26
|
def determine_type
|
26
|
-
if
|
27
|
+
if type.include?('::')
|
27
28
|
# TODO: wrap it up in Virtus.constantize and use feature-detection to
|
28
29
|
# pick up either Inflecto or ActiveSupport, whateve is available
|
29
30
|
if defined?(Inflecto)
|
30
|
-
Inflecto.constantize(
|
31
|
+
Inflecto.constantize(type)
|
31
32
|
else
|
32
33
|
raise NotImplementedError, 'Virtus needs inflecto gem to constantize namespaced constant names'
|
33
34
|
end
|
34
35
|
else
|
35
|
-
Object.const_get(
|
36
|
+
Object.const_get(type)
|
36
37
|
end
|
37
38
|
end
|
38
39
|
|
39
40
|
end # PendingAttribute
|
40
41
|
|
42
|
+
# Extracts the actual type primitive from input type
|
43
|
+
#
|
44
|
+
# @private
|
41
45
|
class TypeDefinition
|
42
46
|
attr_reader :type, :primitive
|
43
47
|
|
@@ -76,13 +80,11 @@ module Virtus
|
|
76
80
|
|
77
81
|
class Attribute
|
78
82
|
|
79
|
-
#
|
80
|
-
# smaller chunks. We probably need some option parser with dedicated
|
81
|
-
# sub-classes per attribute type (different one for Hash, Collection, EV)
|
83
|
+
# Builder is used to set up an attribute instance based on input type and options
|
82
84
|
#
|
83
85
|
# @private
|
84
86
|
class Builder
|
85
|
-
attr_reader :attribute
|
87
|
+
attr_reader :attribute, :options, :type_definition, :klass, :type
|
86
88
|
|
87
89
|
# @api private
|
88
90
|
def self.call(type, options = {})
|
@@ -129,48 +131,54 @@ module Virtus
|
|
129
131
|
|
130
132
|
# @api private
|
131
133
|
def initialize_class
|
132
|
-
@klass = self.class.determine_type(
|
134
|
+
@klass = self.class.determine_type(type_definition.primitive, Attribute)
|
133
135
|
end
|
134
136
|
|
135
137
|
# @api private
|
136
138
|
def initialize_type
|
137
|
-
@type =
|
139
|
+
@type = klass.build_type(type_definition)
|
138
140
|
end
|
139
141
|
|
140
142
|
# @api private
|
141
143
|
def initialize_options(options)
|
142
|
-
@options =
|
143
|
-
|
144
|
-
determine_visibility
|
145
|
-
end
|
146
|
-
|
147
|
-
# @api private
|
148
|
-
def determine_visibility!
|
149
|
-
default_accessor = @options.fetch(:accessor)
|
150
|
-
reader_visibility = @options.fetch(:reader, default_accessor)
|
151
|
-
writer_visibility = @options.fetch(:writer, default_accessor)
|
152
|
-
@options.update(:reader => reader_visibility, :writer => writer_visibility)
|
144
|
+
@options = klass.options.merge(:coerce => Virtus.coerce).update(options)
|
145
|
+
klass.merge_options!(type, @options)
|
146
|
+
determine_visibility
|
153
147
|
end
|
154
148
|
|
155
149
|
# @api private
|
156
150
|
def initialize_default_value
|
157
|
-
|
151
|
+
options.update(:default_value => DefaultValue.build(options[:default]))
|
158
152
|
end
|
159
153
|
|
160
154
|
# @api private
|
161
155
|
def initialize_coercer
|
162
|
-
|
156
|
+
options.update(:coercer => determine_coercer)
|
163
157
|
end
|
164
158
|
|
165
159
|
# @api private
|
166
160
|
def initialize_attribute
|
167
|
-
@attribute =
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
161
|
+
@attribute = klass.new(type, options)
|
162
|
+
|
163
|
+
@attribute.extend(Accessor) if options[:name]
|
164
|
+
@attribute.extend(Coercible) if options[:coerce]
|
165
|
+
@attribute.extend(Strict) if options[:strict]
|
166
|
+
@attribute.extend(LazyDefault) if options[:lazy]
|
167
|
+
|
168
|
+
@attribute.finalize if options[:finalize]
|
169
|
+
end
|
170
|
+
|
171
|
+
# @api private
|
172
|
+
def determine_coercer
|
173
|
+
options.fetch(:coercer) { klass.build_coercer(type, options) }
|
174
|
+
end
|
175
|
+
|
176
|
+
# @api private
|
177
|
+
def determine_visibility
|
178
|
+
default_accessor = options.fetch(:accessor)
|
179
|
+
reader_visibility = options.fetch(:reader, default_accessor)
|
180
|
+
writer_visibility = options.fetch(:writer, default_accessor)
|
181
|
+
options.update(:reader => reader_visibility, :writer => writer_visibility)
|
174
182
|
end
|
175
183
|
|
176
184
|
end # class Builder
|
@@ -40,10 +40,17 @@ module Virtus
|
|
40
40
|
def self.infer_member_type(type)
|
41
41
|
return unless type.respond_to?(:count)
|
42
42
|
|
43
|
-
|
44
|
-
|
43
|
+
member_type =
|
44
|
+
if type.count > 1
|
45
|
+
raise NotImplementedError, "build SumType from list of types (#{type})"
|
46
|
+
else
|
47
|
+
type.first
|
48
|
+
end
|
49
|
+
|
50
|
+
if member_type.is_a?(Class) && member_type < Attribute && member_type.primitive
|
51
|
+
member_type.primitive
|
45
52
|
else
|
46
|
-
|
53
|
+
member_type
|
47
54
|
end
|
48
55
|
end
|
49
56
|
|
@@ -59,9 +66,7 @@ module Virtus
|
|
59
66
|
|
60
67
|
# @api private
|
61
68
|
def self.merge_options!(type, options)
|
62
|
-
|
63
|
-
options[:member_type] = Attribute.build(type.member_type)
|
64
|
-
end
|
69
|
+
options[:member_type] ||= Attribute.build(type.member_type)
|
65
70
|
end
|
66
71
|
|
67
72
|
# @api public
|
@@ -25,6 +25,9 @@ module Virtus
|
|
25
25
|
class EmbeddedValue < Attribute
|
26
26
|
TYPES = [Struct, OpenStruct, Virtus, Model::Constructor].freeze
|
27
27
|
|
28
|
+
# Abstract EV coercer class
|
29
|
+
#
|
30
|
+
# @private
|
28
31
|
class Coercer
|
29
32
|
attr_reader :primitive
|
30
33
|
|
@@ -34,6 +37,10 @@ module Virtus
|
|
34
37
|
|
35
38
|
end # Coercer
|
36
39
|
|
40
|
+
# Builds Struct-like instance with attributes passed to the constructor as
|
41
|
+
# a list of args rather than a hash
|
42
|
+
#
|
43
|
+
# @private
|
37
44
|
class FromStruct < Coercer
|
38
45
|
|
39
46
|
# @api public
|
@@ -47,6 +54,10 @@ module Virtus
|
|
47
54
|
|
48
55
|
end # FromStruct
|
49
56
|
|
57
|
+
# Builds OpenStruct-like instance with attributes passed to the constructor
|
58
|
+
# as a hash
|
59
|
+
#
|
60
|
+
# @private
|
50
61
|
class FromOpenStruct < Coercer
|
51
62
|
|
52
63
|
# @api public
|
@@ -67,8 +78,7 @@ module Virtus
|
|
67
78
|
|
68
79
|
# @api private
|
69
80
|
def self.build_type(definition)
|
70
|
-
|
71
|
-
Axiom::Types::Object.new { primitive klass }
|
81
|
+
Axiom::Types::Object.new { primitive definition.primitive }
|
72
82
|
end
|
73
83
|
|
74
84
|
# @api private
|
@@ -82,11 +92,6 @@ module Virtus
|
|
82
92
|
end
|
83
93
|
end
|
84
94
|
|
85
|
-
# @api public
|
86
|
-
def primitive
|
87
|
-
type.primitive
|
88
|
-
end
|
89
|
-
|
90
95
|
end # class EmbeddedValue
|
91
96
|
|
92
97
|
end # class Attribute
|
@@ -57,7 +57,23 @@ module Virtus
|
|
57
57
|
if type.size > 1
|
58
58
|
raise ArgumentError, "more than one [key => value] pair in `#{type}`"
|
59
59
|
else
|
60
|
-
|
60
|
+
key_type, value_type = type.keys.first, type.values.first
|
61
|
+
|
62
|
+
key_primitive =
|
63
|
+
if key_type.is_a?(Class) && key_type < Attribute && key_type.primitive
|
64
|
+
key_type.primitive
|
65
|
+
else
|
66
|
+
key_type
|
67
|
+
end
|
68
|
+
|
69
|
+
value_primitive =
|
70
|
+
if value_type.is_a?(Class) && value_type < Attribute && value_type.primitive
|
71
|
+
value_type.primitive
|
72
|
+
else
|
73
|
+
value_type
|
74
|
+
end
|
75
|
+
|
76
|
+
{ :key_type => key_primitive, :value_type => value_primitive}
|
61
77
|
end
|
62
78
|
end
|
63
79
|
|
@@ -77,13 +93,8 @@ module Virtus
|
|
77
93
|
|
78
94
|
# @api private
|
79
95
|
def self.merge_options!(type, options)
|
80
|
-
|
81
|
-
|
82
|
-
end
|
83
|
-
|
84
|
-
unless options.key?(:value_type)
|
85
|
-
options[:value_type] = Attribute.build(type.value_type)
|
86
|
-
end
|
96
|
+
options[:key_type] ||= Attribute.build(type.key_type)
|
97
|
+
options[:value_type] ||= Attribute.build(type.value_type)
|
87
98
|
end
|
88
99
|
|
89
100
|
# @api public
|
data/lib/virtus/attribute_set.rb
CHANGED
@@ -182,22 +182,11 @@ module Virtus
|
|
182
182
|
# @api private
|
183
183
|
def set_defaults(object, filter = method(:skip_default?))
|
184
184
|
each do |attribute|
|
185
|
-
if filter.call(object, attribute)
|
186
|
-
|
187
|
-
end
|
188
|
-
set_default(object, attribute)
|
185
|
+
next if filter.call(object, attribute)
|
186
|
+
attribute.set_default_value(object)
|
189
187
|
end
|
190
188
|
end
|
191
189
|
|
192
|
-
# Set default attribute
|
193
|
-
#
|
194
|
-
# @return [default value]
|
195
|
-
#
|
196
|
-
# @api private
|
197
|
-
def set_default(object, attribute)
|
198
|
-
attribute.set_default_value(object)
|
199
|
-
end
|
200
|
-
|
201
190
|
# Coerce attributes received to a hash
|
202
191
|
#
|
203
192
|
# @return [Hash]
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module Virtus
|
2
|
+
|
3
|
+
# Class to build a Virtus module with it's own config
|
4
|
+
#
|
5
|
+
# This allows for individual Virtus modules to be included in
|
6
|
+
# classes and not impacted by the global Virtus config,
|
7
|
+
# which is implemented using Virtus::config.
|
8
|
+
#
|
9
|
+
# @private
|
10
|
+
class Builder
|
11
|
+
|
12
|
+
# Return module
|
13
|
+
#
|
14
|
+
# @return [Module]
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
attr_reader :mod
|
18
|
+
|
19
|
+
# Return config
|
20
|
+
#
|
21
|
+
# @return [config]
|
22
|
+
#
|
23
|
+
# @api private
|
24
|
+
attr_reader :config
|
25
|
+
|
26
|
+
# @api private
|
27
|
+
def self.call(options, &block)
|
28
|
+
new(Configuration.build(options, &block)).mod
|
29
|
+
end
|
30
|
+
|
31
|
+
# @api private
|
32
|
+
def self.pending
|
33
|
+
@pending ||= []
|
34
|
+
end
|
35
|
+
|
36
|
+
# Initializes a new Builder
|
37
|
+
#
|
38
|
+
# @param [Configuration] config
|
39
|
+
# @param [Module] mod
|
40
|
+
#
|
41
|
+
# @return [undefined]
|
42
|
+
#
|
43
|
+
# @api private
|
44
|
+
def initialize(conf, mod = Module.new)
|
45
|
+
@config, @mod = conf, mod
|
46
|
+
add_included_hook
|
47
|
+
add_extended_hook
|
48
|
+
end
|
49
|
+
|
50
|
+
# @api private
|
51
|
+
def extensions
|
52
|
+
[Model::Core]
|
53
|
+
end
|
54
|
+
|
55
|
+
# @api private
|
56
|
+
def options
|
57
|
+
config.to_h
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Adds the .included hook to the anonymous module which then defines the
|
63
|
+
# .attribute method to override the default.
|
64
|
+
#
|
65
|
+
# @return [Module]
|
66
|
+
#
|
67
|
+
# @api private
|
68
|
+
def add_included_hook
|
69
|
+
with_hook_context do |context|
|
70
|
+
mod.define_singleton_method :included do |object|
|
71
|
+
Builder.pending << object unless context.finalize?
|
72
|
+
context.modules.each { |mod| object.send(:include, mod) }
|
73
|
+
object.define_singleton_method(:attribute, context.attribute_method)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# @api private
|
79
|
+
def add_extended_hook
|
80
|
+
with_hook_context do |context|
|
81
|
+
mod.define_singleton_method :extended do |object|
|
82
|
+
context.modules.each { |mod| object.extend(mod) }
|
83
|
+
object.define_singleton_method(:attribute, context.attribute_method)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# @api private
|
89
|
+
def with_hook_context
|
90
|
+
yield(HookContext.new(self, config))
|
91
|
+
end
|
92
|
+
|
93
|
+
end # class Builder
|
94
|
+
|
95
|
+
# @private
|
96
|
+
class ModelBuilder < Builder
|
97
|
+
end # ModelBuilder
|
98
|
+
|
99
|
+
# @private
|
100
|
+
class ModuleBuilder < Builder
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
# @api private
|
105
|
+
def add_included_hook
|
106
|
+
with_hook_context do |context|
|
107
|
+
mod.define_singleton_method :included do |object|
|
108
|
+
super(object)
|
109
|
+
object.extend(ModuleExtensions)
|
110
|
+
ModuleExtensions.setup(object, context.modules)
|
111
|
+
object.define_singleton_method(:attribute, context.attribute_method)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
end # ModuleBuilder
|
117
|
+
|
118
|
+
# @private
|
119
|
+
class ValueObjectBuilder < Builder
|
120
|
+
|
121
|
+
# @api private
|
122
|
+
def extensions
|
123
|
+
super + [
|
124
|
+
Extensions::AllowedWriterMethods,
|
125
|
+
ValueObject::AllowedWriterMethods,
|
126
|
+
ValueObject::InstanceMethods
|
127
|
+
]
|
128
|
+
end
|
129
|
+
|
130
|
+
# @api private
|
131
|
+
def options
|
132
|
+
super.merge(:writer => :private)
|
133
|
+
end
|
134
|
+
|
135
|
+
end # ValueObjectBuilder
|
136
|
+
|
137
|
+
end # module Virtus
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Virtus
|
2
|
+
class Builder
|
3
|
+
|
4
|
+
# Context used for building "included" and "extended" hooks
|
5
|
+
#
|
6
|
+
# @private
|
7
|
+
class HookContext
|
8
|
+
attr_reader :builder, :config, :attribute_method
|
9
|
+
|
10
|
+
# @api private
|
11
|
+
def initialize(builder, config)
|
12
|
+
@builder, @config = builder, config
|
13
|
+
initialize_attribute_method
|
14
|
+
end
|
15
|
+
|
16
|
+
# @api private
|
17
|
+
def modules
|
18
|
+
modules = builder.extensions
|
19
|
+
modules << Model::Constructor if constructor?
|
20
|
+
modules << Model::MassAssignment if mass_assignment?
|
21
|
+
modules
|
22
|
+
end
|
23
|
+
|
24
|
+
# @api private
|
25
|
+
def constructor?
|
26
|
+
config.constructor
|
27
|
+
end
|
28
|
+
|
29
|
+
# @api private
|
30
|
+
def mass_assignment?
|
31
|
+
config.mass_assignment
|
32
|
+
end
|
33
|
+
|
34
|
+
# @api private
|
35
|
+
def finalize?
|
36
|
+
config.finalize
|
37
|
+
end
|
38
|
+
|
39
|
+
# @api private
|
40
|
+
def initialize_attribute_method
|
41
|
+
method_options = builder.options
|
42
|
+
|
43
|
+
@attribute_method = lambda do |name, type = nil, options = {}|
|
44
|
+
super(name, type, method_options.merge(options))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end # HookContext
|
49
|
+
|
50
|
+
end # Builder
|
51
|
+
end # Virtus
|