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.
@@ -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 = type
10
- @options = options
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, @options).finalize
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 @type.include?('::')
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(@type)
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(@type)
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
- # TODO: this is a huge class and it might be a good idea to split it into
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(@type_definition.primitive, Attribute)
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 = @klass.build_type(@type_definition)
139
+ @type = klass.build_type(type_definition)
138
140
  end
139
141
 
140
142
  # @api private
141
143
  def initialize_options(options)
142
- @options = @klass.options.merge(:coerce => Virtus.coerce).update(options)
143
- @klass.merge_options!(@type, @options)
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
- @options.update(:default_value => DefaultValue.build(@options[:default]))
151
+ options.update(:default_value => DefaultValue.build(options[:default]))
158
152
  end
159
153
 
160
154
  # @api private
161
155
  def initialize_coercer
162
- @options.update(:coercer => @options.fetch(:coercer) { @klass.build_coercer(@type, @options) })
156
+ options.update(:coercer => determine_coercer)
163
157
  end
164
158
 
165
159
  # @api private
166
160
  def initialize_attribute
167
- @attribute = @klass.new(@type, @options) do |attribute|
168
- attribute.extend(Accessor) if @options[:name]
169
- attribute.extend(Coercible) if @options[:coerce]
170
- attribute.extend(Strict) if @options[:strict]
171
- attribute.extend(LazyDefault) if @options[:lazy]
172
- end
173
- @attribute.finalize if @options[:finalize]
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
- if type.count > 1
44
- raise NotImplementedError, "build SumType from list of types (#{type})"
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
- type.first
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
- unless options.key?(:member_type)
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
- klass = definition.primitive.is_a?(Class) ? definition.primitive : definition.type
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
- { :key_type => type.keys.first, :value_type => type.values.first }
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
- unless options.key?(:key_type)
81
- options[:key_type] = Attribute.build(type.key_type)
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
@@ -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
- next
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