virtus 1.0.0.beta7 → 1.0.0.beta8

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/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