virtus 1.0.0.beta8 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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