virtus 1.0.0.beta7 → 1.0.0.beta8

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -476,6 +476,9 @@ class User
476
476
  end
477
477
  ```
478
478
 
479
+ Please check out [Coercible README](https://github.com/solnic/coercible/blob/master/README.md)
480
+ for more information.
481
+
479
482
  ## Strict Coercion Mode
480
483
 
481
484
  By default Virtus returns the input value even when it couldn't coerce it to the expected type.
@@ -519,8 +522,34 @@ class User
519
522
  end
520
523
  ```
521
524
 
522
- Please check out [Coercible README](https://github.com/solnic/coercible/blob/master/README.md)
523
- for more information.
525
+ ## Attribute Finalization and Circular Dependencies
526
+
527
+ If a type references another type which happens to not be available yet you need
528
+ to use lazy-finalization of attributes and finalize virtus manually after all
529
+ types have been already loaded:
530
+
531
+ ``` ruby
532
+ # in blog.rb
533
+ class Blog
534
+ include Virtus.model(:finalize => false)
535
+
536
+ attribute :posts, Array['Post']
537
+ end
538
+
539
+ # in post.rb
540
+ class Post
541
+ include Virtus.model(:finalize => false)
542
+
543
+ attribute :blog, 'Blog'
544
+ end
545
+
546
+ # after loading both files just do:
547
+ Virtus.finalize
548
+
549
+ # constants will be resolved:
550
+ Blog.attribute_set[:posts].member_type.primitive # => Post
551
+ Post.attribute_set[:blog].type.primitive # => Blog
552
+ ```
524
553
 
525
554
  Credits
526
555
  -------
@@ -150,6 +150,13 @@ module Virtus
150
150
  @configuration ||= Configuration.new
151
151
  end
152
152
 
153
+ # @api public
154
+ def self.finalize
155
+ ExtensionBuilder.pending.each do |klass|
156
+ klass.attribute_set.finalize
157
+ end
158
+ end
159
+
153
160
  # @api private
154
161
  def self.warn(msg)
155
162
  Kernel.warn(msg)
@@ -5,11 +5,12 @@ module Virtus
5
5
 
6
6
  include ::Equalizer.new(:type, :options)
7
7
 
8
- accept_options :primitive, :accessor, :default, :lazy, :strict, :required
8
+ accept_options :primitive, :accessor, :default, :lazy, :strict, :required, :finalize
9
9
 
10
10
  strict false
11
11
  required true
12
12
  accessor :public
13
+ finalize true
13
14
 
14
15
  # @see Virtus.coerce
15
16
  #
@@ -55,12 +56,12 @@ module Virtus
55
56
  def self.new(*args)
56
57
  attribute = super
57
58
  yield(attribute)
58
- attribute.finalize
59
+ attribute
59
60
  end
60
61
 
61
62
  # @api private
62
- def self.build_type(options)
63
- Axiom::Types.infer(options[:primitive])
63
+ def self.build_type(definition)
64
+ Axiom::Types.infer(definition.primitive)
64
65
  end
65
66
 
66
67
  # @api private
@@ -126,6 +127,11 @@ module Virtus
126
127
  options[:required]
127
128
  end
128
129
 
130
+ # @api public
131
+ def finalized?
132
+ frozen?
133
+ end
134
+
129
135
  # @api private
130
136
  def define_accessor_methods(attribute_set)
131
137
  attribute_set.define_reader_method(self, name, options[:reader])
@@ -135,6 +141,7 @@ module Virtus
135
141
  # @api private
136
142
  def finalize
137
143
  freeze
144
+ self
138
145
  end
139
146
 
140
147
  end # class Attribute
@@ -1,4 +1,79 @@
1
1
  module Virtus
2
+
3
+ # @private
4
+ class PendingAttribute
5
+ attr_reader :name
6
+
7
+ # @api private
8
+ def initialize(type, options)
9
+ @type = type
10
+ @options = options
11
+ @name = options[:name]
12
+ end
13
+
14
+ # @api private
15
+ def finalize
16
+ Attribute::Builder.call(determine_type, @options).finalize
17
+ end
18
+
19
+ # @api private
20
+ def finalized?
21
+ false
22
+ end
23
+
24
+ # @api private
25
+ def determine_type
26
+ if @type.include?('::')
27
+ # TODO: wrap it up in Virtus.constantize and use feature-detection to
28
+ # pick up either Inflecto or ActiveSupport, whateve is available
29
+ if defined?(Inflecto)
30
+ Inflecto.constantize(@type)
31
+ else
32
+ raise NotImplementedError, 'Virtus needs inflecto gem to constantize namespaced constant names'
33
+ end
34
+ else
35
+ Object.const_get(@type)
36
+ end
37
+ end
38
+
39
+ end # PendingAttribute
40
+
41
+ class TypeDefinition
42
+ attr_reader :type, :primitive
43
+
44
+ # @api private
45
+ def initialize(type)
46
+ @type = type
47
+ initialize_primitive
48
+ end
49
+
50
+ # @api private
51
+ def pending?
52
+ @pending
53
+ end
54
+
55
+ private
56
+
57
+ # @api private
58
+ def initialize_primitive
59
+ @primitive =
60
+ if type.instance_of?(String) || type.instance_of?(Symbol)
61
+ if !type.to_s.include?('::') && Object.const_defined?(type)
62
+ Object.const_get(type)
63
+ elsif not Attribute::Builder.determine_type(type)
64
+ @pending = true
65
+ type
66
+ else
67
+ type
68
+ end
69
+ elsif not type.is_a?(Class)
70
+ type.class
71
+ else
72
+ type
73
+ end
74
+ end
75
+ end
76
+
2
77
  class Attribute
3
78
 
4
79
  # TODO: this is a huge class and it might be a good idea to split it into
@@ -10,8 +85,14 @@ module Virtus
10
85
  attr_reader :attribute
11
86
 
12
87
  # @api private
13
- def self.call(*args)
14
- new(*args).attribute
88
+ def self.call(type, options = {})
89
+ type_definition = TypeDefinition.new(type)
90
+
91
+ if type_definition.pending?
92
+ PendingAttribute.new(type, options)
93
+ else
94
+ new(type_definition, options).attribute
95
+ end
15
96
  end
16
97
 
17
98
  # @api private
@@ -33,10 +114,11 @@ module Virtus
33
114
  end
34
115
 
35
116
  # @api private
36
- def initialize(type, options)
37
- initialize_primitive(type)
117
+ def initialize(type_definition, options)
118
+ @type_definition = type_definition
119
+
38
120
  initialize_class
39
- initialize_type(:type => type, :primitive => @primitive)
121
+ initialize_type
40
122
  initialize_options(options)
41
123
  initialize_default_value
42
124
  initialize_coercer
@@ -45,50 +127,14 @@ module Virtus
45
127
 
46
128
  private
47
129
 
48
- # @api private
49
- def initialize_attribute
50
- @attribute = @klass.new(@type, @options) do |attribute|
51
- attribute.extend(Accessor) if @options[:name]
52
- attribute.extend(Coercible) if @options[:coerce]
53
- attribute.extend(Strict) if @options[:strict]
54
- attribute.extend(LazyDefault) if @options[:lazy]
55
- end
56
- end
57
-
58
- # @api private
59
- def initialize_default_value
60
- @options.update(:default_value => DefaultValue.build(@options[:default]))
61
- end
62
-
63
- # @api private
64
- def initialize_coercer
65
- @options.update(:coercer => @options.fetch(:coercer) { @klass.build_coercer(@type, @options) })
66
- end
67
-
68
- # @api private
69
- def initialize_primitive(type)
70
- @primitive =
71
- if type.instance_of?(String) || type.instance_of?(Symbol)
72
- begin
73
- Object.const_get(type)
74
- rescue
75
- type
76
- end
77
- elsif not type.is_a?(Class)
78
- type.class
79
- else
80
- type
81
- end
82
- end
83
-
84
130
  # @api private
85
131
  def initialize_class
86
- @klass = self.class.determine_type(@primitive, Attribute)
132
+ @klass = self.class.determine_type(@type_definition.primitive, Attribute)
87
133
  end
88
134
 
89
135
  # @api private
90
- def initialize_type(options)
91
- @type = @klass.build_type(options)
136
+ def initialize_type
137
+ @type = @klass.build_type(@type_definition)
92
138
  end
93
139
 
94
140
  # @api private
@@ -106,6 +152,27 @@ module Virtus
106
152
  @options.update(:reader => reader_visibility, :writer => writer_visibility)
107
153
  end
108
154
 
155
+ # @api private
156
+ def initialize_default_value
157
+ @options.update(:default_value => DefaultValue.build(@options[:default]))
158
+ end
159
+
160
+ # @api private
161
+ def initialize_coercer
162
+ @options.update(:coercer => @options.fetch(:coercer) { @klass.build_coercer(@type, @options) })
163
+ end
164
+
165
+ # @api private
166
+ 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]
174
+ end
175
+
109
176
  end # class Builder
110
177
 
111
178
  end # class Attribute
@@ -9,15 +9,17 @@ module Virtus
9
9
  class Collection < Attribute
10
10
  default Proc.new { |_, attribute| attribute.type.primitive.new }
11
11
 
12
+ attr_reader :member_type
13
+
12
14
  # FIXME: temporary hack, remove when Axiom::Type works with EV as member_type
13
15
  Type = Struct.new(:primitive, :member_type) do
14
16
  def self.infer(type, primitive)
15
- return type if type.is_a?(Class) && type < Axiom::Types::Type
17
+ return type if axiom_type?(type)
16
18
 
17
19
  klass = Axiom::Types.infer(type)
18
20
  member = infer_member_type(type) || Object
19
21
 
20
- if EmbeddedValue.handles?(member)
22
+ if EmbeddedValue.handles?(member) || pending?(member)
21
23
  Type.new(primitive, member)
22
24
  else
23
25
  klass.new {
@@ -27,6 +29,14 @@ module Virtus
27
29
  end
28
30
  end
29
31
 
32
+ def self.pending?(primitive)
33
+ primitive.is_a?(String) || primitive.is_a?(Symbol)
34
+ end
35
+
36
+ def self.axiom_type?(type)
37
+ type.is_a?(Class) && type < Axiom::Types::Type
38
+ end
39
+
30
40
  def self.infer_member_type(type)
31
41
  return unless type.respond_to?(:count)
32
42
 
@@ -43,8 +53,8 @@ module Virtus
43
53
  end
44
54
 
45
55
  # @api private
46
- def self.build_type(options)
47
- Type.infer(options.fetch(:type), options.fetch(:primitive))
56
+ def self.build_type(definition)
57
+ Type.infer(definition.type, definition.primitive)
48
58
  end
49
59
 
50
60
  # @api private
@@ -61,10 +71,19 @@ module Virtus
61
71
  end
62
72
  end
63
73
 
64
- def member_type
65
- @options[:member_type]
74
+ # @api private
75
+ def finalize
76
+ return self if finalized?
77
+ @member_type = @options[:member_type].finalize
78
+ super
79
+ end
80
+
81
+ # @api private
82
+ def finalized?
83
+ super && member_type.finalized?
66
84
  end
67
85
 
68
86
  end # class Collection
87
+
69
88
  end # class Attribute
70
89
  end # module Virtus
@@ -66,8 +66,9 @@ module Virtus
66
66
  end
67
67
 
68
68
  # @api private
69
- def self.build_type(options)
70
- Axiom::Types::Object.new { primitive options[:type] }
69
+ def self.build_type(definition)
70
+ klass = definition.primitive.is_a?(Class) ? definition.primitive : definition.type
71
+ Axiom::Types::Object.new { primitive klass }
71
72
  end
72
73
 
73
74
  # @api private
@@ -16,10 +16,13 @@ module Virtus
16
16
  primitive ::Hash
17
17
  default primitive.new
18
18
 
19
+ attr_reader :key_type
20
+ attr_reader :value_type
21
+
19
22
  # FIXME: remove this once axiom-types supports it
20
23
  Type = Struct.new(:key_type, :value_type) do
21
24
  def self.infer(type)
22
- if type.is_a?(Class) && type < Axiom::Types::Type
25
+ if axiom_type?(type)
23
26
  new(type.key_type, type.value_type)
24
27
  else
25
28
  type_options = infer_key_and_value_types(type)
@@ -30,7 +33,17 @@ module Virtus
30
33
  end
31
34
  end
32
35
 
36
+ def self.pending?(primitive)
37
+ primitive.is_a?(String) || primitive.is_a?(Symbol)
38
+ end
39
+
40
+ def self.axiom_type?(type)
41
+ type.is_a?(Class) && type < Axiom::Types::Type
42
+ end
43
+
33
44
  def self.determine_type(type)
45
+ return type if pending?(type)
46
+
34
47
  if EmbeddedValue.handles?(type)
35
48
  type
36
49
  else
@@ -58,8 +71,8 @@ module Virtus
58
71
  end
59
72
 
60
73
  # @api private
61
- def self.build_type(options)
62
- Type.infer(options[:type])
74
+ def self.build_type(definition)
75
+ Type.infer(definition.type)
63
76
  end
64
77
 
65
78
  # @api private
@@ -85,13 +98,16 @@ module Virtus
85
98
  end
86
99
 
87
100
  # @api private
88
- def key_type
89
- @options[:key_type]
101
+ def finalize
102
+ return self if finalized?
103
+ @key_type = options[:key_type].finalize
104
+ @value_type = options[:value_type].finalize
105
+ super
90
106
  end
91
107
 
92
108
  # @api private
93
- def value_type
94
- @options[:value_type]
109
+ def finalized?
110
+ super && key_type.finalized? && value_type.finalized?
95
111
  end
96
112
 
97
113
  end # class Hash
@@ -74,7 +74,8 @@ module Virtus
74
74
  # @api public
75
75
  def <<(attribute)
76
76
  self[attribute.name] = attribute
77
- attribute.define_accessor_methods(self)
77
+ attribute.define_accessor_methods(self) if attribute.finalized?
78
+ self
78
79
  end
79
80
 
80
81
  # Get an attribute by name
@@ -208,6 +209,13 @@ module Virtus
208
209
  )
209
210
  end
210
211
 
212
+ # @api private
213
+ def finalize
214
+ each do |attribute|
215
+ self << attribute.finalize unless attribute.finalized?
216
+ end
217
+ end
218
+
211
219
  private
212
220
 
213
221
  # @api private
@@ -3,6 +3,9 @@ module Virtus
3
3
  # A Configuration instance
4
4
  class Configuration
5
5
 
6
+ # Access the finalize setting for this instance
7
+ attr_accessor :finalize
8
+
6
9
  # Access the coerce setting for this instance
7
10
  attr_accessor :coerce
8
11
 
@@ -35,6 +38,7 @@ module Virtus
35
38
  #
36
39
  # @api private
37
40
  def initialize
41
+ @finalize = true
38
42
  @coerce = true
39
43
  @strict = false
40
44
  @constructor = true
@@ -44,6 +44,11 @@ module Virtus
44
44
  builder.module
45
45
  end
46
46
 
47
+ # @api private
48
+ def self.pending
49
+ @pending ||= []
50
+ end
51
+
47
52
  # Initializes a new ModuleBuilder
48
53
  #
49
54
  # @param [Configuration] configuration
@@ -68,11 +73,13 @@ module Virtus
68
73
  attribute_proc = attribute_method(configuration)
69
74
  constructor = configuration.constructor
70
75
  mass_assignment = configuration.mass_assignment
76
+ finalize = configuration.finalize
71
77
  extensions = core_extensions
72
78
  inclusions = core_inclusions
73
79
 
74
80
  self.module.define_singleton_method :included do |object|
75
81
  super(object)
82
+ ExtensionBuilder.pending << object unless finalize
76
83
  extensions.each { |mod| object.extend(mod) }
77
84
  inclusions.each { |mod| object.send(:include, mod) }
78
85
  object.send(:include, Model::Constructor) if constructor
@@ -114,6 +121,7 @@ module Virtus
114
121
  # @api private
115
122
  def module_options
116
123
  { :coerce => configuration.coerce,
124
+ :finalize => configuration.finalize,
117
125
  :strict => configuration.strict,
118
126
  :configured_coercer => configuration.coercer }
119
127
  end
@@ -1,3 +1,3 @@
1
1
  module Virtus
2
- VERSION = '1.0.0.beta7'
2
+ VERSION = '1.0.0.beta8'
3
3
  end
@@ -19,6 +19,7 @@ end
19
19
  require 'rspec'
20
20
  require 'bogus/rspec'
21
21
  require 'virtus'
22
+ require 'inflecto' # for resolving namespaced constant names
22
23
 
23
24
  module Virtus
24
25
  def self.warn(*)
@@ -17,6 +17,7 @@ describe Virtus::Attribute::Collection, '#coerce' do
17
17
 
18
18
  it 'uses coercer to coerce members' do
19
19
  stub(coercer).call(input) { input }
20
+ stub(member_type).finalize { member_type }
20
21
  stub(member_type).coerce('1') { 1 }
21
22
  stub(member_type).coerce('2') { 2 }
22
23
 
@@ -41,9 +41,11 @@ describe Virtus::Attribute::Hash, '#coerce' do
41
41
  it 'uses coercer to coerce key and value' do
42
42
  stub(coercer).call(input) { input }
43
43
 
44
+ stub(key_type).finalize { key_type }
44
45
  stub(key_type).coerce(1) { '1' }
45
46
  stub(key_type).coerce(2) { '2' }
46
47
 
48
+ stub(value_type).finalize { value_type }
47
49
  stub(value_type).coerce('1') { 1 }
48
50
  stub(value_type).coerce('2') { 2 }
49
51
 
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus, '.finalize' do
4
+ before do
5
+ module Examples
6
+ class Person
7
+ include Virtus.model(:finalize => false)
8
+
9
+ attribute :articles, Array['Examples::Article']
10
+ attribute :address, 'Examples::Address'
11
+ end
12
+
13
+ class Article
14
+ include Virtus.model(:finalize => false)
15
+
16
+ attribute :posts, Hash['Examples::Person' => 'Examples::Post']
17
+ attribute :person, 'Examples::Person'
18
+ end
19
+
20
+ class Post
21
+ include Virtus.model(:finalize => false)
22
+
23
+ attribute :title, String
24
+ end
25
+
26
+ class Address
27
+ include Virtus.model(:finalize => false)
28
+
29
+ attribute :street, String
30
+ end
31
+ end
32
+
33
+ Virtus.finalize
34
+ end
35
+
36
+ it 'it finalizes member type for a collection attribute' do
37
+ expect(Examples::Person.attribute_set[:address].primitive).to be(Examples::Address)
38
+ end
39
+
40
+ it 'it finalizes key type for a hash attribute' do
41
+ expect(Examples::Article.attribute_set[:posts].key_type.primitive).to be(Examples::Person)
42
+ end
43
+
44
+ it 'it finalizes value type for a hash attribute' do
45
+ expect(Examples::Article.attribute_set[:posts].value_type.primitive).to be(Examples::Post)
46
+ end
47
+
48
+ it 'it finalizes type for an EV attribute' do
49
+ expect(Examples::Article.attribute_set[:person].type.primitive).to be(Examples::Person)
50
+ end
51
+
52
+ it 'automatically resolves constant when it is already available' do
53
+ expect(Examples::Article.attribute_set[:person].type.primitive).to be(Examples::Person)
54
+ end
55
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: virtus
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.beta7
4
+ version: 1.0.0.beta8
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-09-26 00:00:00.000000000 Z
12
+ date: 2013-09-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: descendants_tracker
@@ -189,6 +189,7 @@ files:
189
189
  - spec/unit/virtus/attribute_spec.rb
190
190
  - spec/unit/virtus/attributes_reader_spec.rb
191
191
  - spec/unit/virtus/attributes_writer_spec.rb
192
+ - spec/unit/virtus/class_methods/finalize_spec.rb
192
193
  - spec/unit/virtus/class_methods/new_spec.rb
193
194
  - spec/unit/virtus/config_spec.rb
194
195
  - spec/unit/virtus/element_reader_spec.rb