virtus2 2.0.1

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.
Files changed (118) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +39 -0
  3. data/.rspec +2 -0
  4. data/.yardopts +1 -0
  5. data/CONTRIBUTING.md +18 -0
  6. data/Changelog.md +258 -0
  7. data/Gemfile +10 -0
  8. data/Guardfile +19 -0
  9. data/LICENSE +20 -0
  10. data/README.md +630 -0
  11. data/Rakefile +15 -0
  12. data/TODO.md +6 -0
  13. data/lib/virtus/attribute/accessor.rb +103 -0
  14. data/lib/virtus/attribute/boolean.rb +55 -0
  15. data/lib/virtus/attribute/builder.rb +182 -0
  16. data/lib/virtus/attribute/coercer.rb +45 -0
  17. data/lib/virtus/attribute/coercible.rb +20 -0
  18. data/lib/virtus/attribute/collection.rb +103 -0
  19. data/lib/virtus/attribute/default_value/from_callable.rb +35 -0
  20. data/lib/virtus/attribute/default_value/from_clonable.rb +35 -0
  21. data/lib/virtus/attribute/default_value/from_symbol.rb +35 -0
  22. data/lib/virtus/attribute/default_value.rb +51 -0
  23. data/lib/virtus/attribute/embedded_value.rb +67 -0
  24. data/lib/virtus/attribute/enum.rb +45 -0
  25. data/lib/virtus/attribute/hash.rb +130 -0
  26. data/lib/virtus/attribute/lazy_default.rb +18 -0
  27. data/lib/virtus/attribute/nullify_blank.rb +24 -0
  28. data/lib/virtus/attribute/strict.rb +26 -0
  29. data/lib/virtus/attribute.rb +245 -0
  30. data/lib/virtus/attribute_set.rb +240 -0
  31. data/lib/virtus/builder/hook_context.rb +51 -0
  32. data/lib/virtus/builder.rb +133 -0
  33. data/lib/virtus/class_inclusions.rb +48 -0
  34. data/lib/virtus/class_methods.rb +90 -0
  35. data/lib/virtus/coercer.rb +41 -0
  36. data/lib/virtus/configuration.rb +72 -0
  37. data/lib/virtus/const_missing_extensions.rb +18 -0
  38. data/lib/virtus/extensions.rb +105 -0
  39. data/lib/virtus/instance_methods.rb +218 -0
  40. data/lib/virtus/model.rb +68 -0
  41. data/lib/virtus/module_extensions.rb +88 -0
  42. data/lib/virtus/support/equalizer.rb +128 -0
  43. data/lib/virtus/support/options.rb +113 -0
  44. data/lib/virtus/support/type_lookup.rb +109 -0
  45. data/lib/virtus/value_object.rb +150 -0
  46. data/lib/virtus/version.rb +3 -0
  47. data/lib/virtus.rb +310 -0
  48. data/spec/integration/attributes_attribute_spec.rb +28 -0
  49. data/spec/integration/building_module_spec.rb +90 -0
  50. data/spec/integration/collection_member_coercion_spec.rb +96 -0
  51. data/spec/integration/custom_attributes_spec.rb +42 -0
  52. data/spec/integration/custom_collection_attributes_spec.rb +101 -0
  53. data/spec/integration/default_values_spec.rb +87 -0
  54. data/spec/integration/defining_attributes_spec.rb +86 -0
  55. data/spec/integration/embedded_value_spec.rb +50 -0
  56. data/spec/integration/extending_objects_spec.rb +35 -0
  57. data/spec/integration/hash_attributes_coercion_spec.rb +54 -0
  58. data/spec/integration/inheritance_spec.rb +42 -0
  59. data/spec/integration/injectible_coercers_spec.rb +48 -0
  60. data/spec/integration/mass_assignment_with_accessors_spec.rb +44 -0
  61. data/spec/integration/overriding_virtus_spec.rb +46 -0
  62. data/spec/integration/required_attributes_spec.rb +25 -0
  63. data/spec/integration/struct_as_embedded_value_spec.rb +28 -0
  64. data/spec/integration/using_modules_spec.rb +55 -0
  65. data/spec/integration/value_object_with_custom_constructor_spec.rb +42 -0
  66. data/spec/integration/virtus/instance_level_attributes_spec.rb +23 -0
  67. data/spec/integration/virtus/value_object_spec.rb +99 -0
  68. data/spec/shared/constants_helpers.rb +9 -0
  69. data/spec/shared/freeze_method_behavior.rb +40 -0
  70. data/spec/shared/idempotent_method_behaviour.rb +5 -0
  71. data/spec/shared/options_class_method.rb +19 -0
  72. data/spec/spec_helper.rb +41 -0
  73. data/spec/unit/virtus/attribute/boolean/coerce_spec.rb +43 -0
  74. data/spec/unit/virtus/attribute/boolean/value_coerced_predicate_spec.rb +25 -0
  75. data/spec/unit/virtus/attribute/class_methods/build_spec.rb +180 -0
  76. data/spec/unit/virtus/attribute/class_methods/coerce_spec.rb +32 -0
  77. data/spec/unit/virtus/attribute/coerce_spec.rb +129 -0
  78. data/spec/unit/virtus/attribute/coercible_predicate_spec.rb +20 -0
  79. data/spec/unit/virtus/attribute/collection/class_methods/build_spec.rb +105 -0
  80. data/spec/unit/virtus/attribute/collection/coerce_spec.rb +74 -0
  81. data/spec/unit/virtus/attribute/collection/value_coerced_predicate_spec.rb +31 -0
  82. data/spec/unit/virtus/attribute/comparison_spec.rb +20 -0
  83. data/spec/unit/virtus/attribute/custom_collection_spec.rb +29 -0
  84. data/spec/unit/virtus/attribute/defined_spec.rb +20 -0
  85. data/spec/unit/virtus/attribute/embedded_value/class_methods/build_spec.rb +70 -0
  86. data/spec/unit/virtus/attribute/embedded_value/coerce_spec.rb +91 -0
  87. data/spec/unit/virtus/attribute/get_spec.rb +32 -0
  88. data/spec/unit/virtus/attribute/hash/class_methods/build_spec.rb +106 -0
  89. data/spec/unit/virtus/attribute/hash/coerce_spec.rb +92 -0
  90. data/spec/unit/virtus/attribute/lazy_predicate_spec.rb +20 -0
  91. data/spec/unit/virtus/attribute/rename_spec.rb +16 -0
  92. data/spec/unit/virtus/attribute/required_predicate_spec.rb +19 -0
  93. data/spec/unit/virtus/attribute/set_default_value_spec.rb +107 -0
  94. data/spec/unit/virtus/attribute/set_spec.rb +29 -0
  95. data/spec/unit/virtus/attribute/value_coerced_predicate_spec.rb +19 -0
  96. data/spec/unit/virtus/attribute_set/append_spec.rb +47 -0
  97. data/spec/unit/virtus/attribute_set/define_reader_method_spec.rb +36 -0
  98. data/spec/unit/virtus/attribute_set/define_writer_method_spec.rb +36 -0
  99. data/spec/unit/virtus/attribute_set/each_spec.rb +65 -0
  100. data/spec/unit/virtus/attribute_set/element_reference_spec.rb +17 -0
  101. data/spec/unit/virtus/attribute_set/element_set_spec.rb +64 -0
  102. data/spec/unit/virtus/attribute_set/merge_spec.rb +34 -0
  103. data/spec/unit/virtus/attribute_set/reset_spec.rb +71 -0
  104. data/spec/unit/virtus/attribute_spec.rb +229 -0
  105. data/spec/unit/virtus/attributes_reader_spec.rb +41 -0
  106. data/spec/unit/virtus/attributes_writer_spec.rb +51 -0
  107. data/spec/unit/virtus/class_methods/finalize_spec.rb +67 -0
  108. data/spec/unit/virtus/class_methods/new_spec.rb +39 -0
  109. data/spec/unit/virtus/config_spec.rb +13 -0
  110. data/spec/unit/virtus/element_reader_spec.rb +21 -0
  111. data/spec/unit/virtus/element_writer_spec.rb +19 -0
  112. data/spec/unit/virtus/freeze_spec.rb +41 -0
  113. data/spec/unit/virtus/model_spec.rb +197 -0
  114. data/spec/unit/virtus/module_spec.rb +174 -0
  115. data/spec/unit/virtus/set_default_attributes_spec.rb +32 -0
  116. data/spec/unit/virtus/value_object_spec.rb +138 -0
  117. data/virtus2.gemspec +26 -0
  118. metadata +225 -0
@@ -0,0 +1,3 @@
1
+ module Virtus
2
+ VERSION = '2.0.1'.freeze
3
+ end
data/lib/virtus.rb ADDED
@@ -0,0 +1,310 @@
1
+ require 'ostruct'
2
+
3
+ # Base module which adds Attribute API to your classes
4
+ module Virtus
5
+
6
+ # Provides args for const_get and const_defined? to make them behave
7
+ # consistently across different versions of ruby
8
+ EXTRA_CONST_ARGS = (RUBY_VERSION < '1.9' ? [] : [ false ]).freeze
9
+
10
+ # Represents an undefined parameter used by auto-generated option methods
11
+ Undefined = Object.new.freeze
12
+
13
+ class CoercionError < StandardError
14
+ attr_reader :output, :attribute
15
+
16
+ def initialize(output, attribute)
17
+ @output, @attribute = output, attribute
18
+ super(build_message)
19
+ end
20
+
21
+ def build_message
22
+ if attribute_name?
23
+ "Failed to coerce attribute `#{attribute_name}' from #{output.inspect} into #{target_type}"
24
+ else
25
+ "Failed to coerce #{output.inspect} into #{target_type}"
26
+ end
27
+ end
28
+
29
+ def attribute_name
30
+ attribute.options[:name]
31
+ end
32
+
33
+ def attribute_name?
34
+ attribute_name ? true : false
35
+ end
36
+
37
+ def target_type
38
+ if attribute.respond_to?(:coercion_error_message)
39
+ attribute.coercion_error_message
40
+ else
41
+ attribute.primitive.inspect
42
+ end
43
+ end
44
+ end
45
+
46
+ # Extends base class or a module with virtus methods
47
+ #
48
+ # @param [Object] object
49
+ #
50
+ # @return [undefined]
51
+ #
52
+ # @deprecated
53
+ #
54
+ # @api private
55
+ def self.included(object)
56
+ super
57
+ if Class === object
58
+ Virtus.warn("including Virtus module is deprecated. Use 'include Virtus.model' instead #{caller.first}")
59
+ object.send(:include, ClassInclusions)
60
+ else
61
+ Virtus.warn("including Virtus module is deprecated. Use 'include Virtus.module' instead #{caller.first}")
62
+ object.extend(ModuleExtensions)
63
+ end
64
+ end
65
+ private_class_method :included
66
+
67
+ # Extends an object with virtus extensions
68
+ #
69
+ # @param [Object] object
70
+ #
71
+ # @return [undefined]
72
+ #
73
+ # @deprecated
74
+ #
75
+ # @api private
76
+ def self.extended(object)
77
+ Virtus.warn("extending with Virtus module is deprecated. Use 'extend(Virtus.model)' instead #{caller.first}")
78
+ object.extend(Extensions)
79
+ end
80
+ private_class_method :extended
81
+
82
+ # Sets the global coercer configuration
83
+ #
84
+ # @example
85
+ # Virtus.coercer do |config|
86
+ # config.string.boolean_map = { true => '1', false => '0' }
87
+ # end
88
+ #
89
+ # @return [Coercible::Coercer]
90
+ #
91
+ # @api public
92
+ def self.coercer(&block)
93
+ configuration.coercer(&block)
94
+ end
95
+
96
+ # Sets the global coercion configuration value
97
+ #
98
+ # @param [Boolean] value
99
+ #
100
+ # @return [Virtus]
101
+ #
102
+ # @api public
103
+ def self.coerce=(value)
104
+ configuration.coerce = value
105
+ self
106
+ end
107
+
108
+ # Returns the global coercion setting
109
+ #
110
+ # @return [Boolean]
111
+ #
112
+ # @api public
113
+ def self.coerce
114
+ configuration.coerce
115
+ end
116
+
117
+ # Provides access to the global Virtus configuration
118
+ #
119
+ # @example
120
+ # Virtus.config do |config|
121
+ # config.coerce = false
122
+ # end
123
+ #
124
+ # @return [Configuration]
125
+ #
126
+ # @api public
127
+ def self.config(&block)
128
+ yield configuration if block_given?
129
+ configuration
130
+ end
131
+
132
+ # Provides access to the Virtus module builder
133
+ # see Virtus::ModuleBuilder
134
+ #
135
+ # @example
136
+ # MyVirtusModule = Virtus.module { |mod|
137
+ # mod.coerce = true
138
+ # mod.string.boolean_map = { 'yup' => true, 'nope' => false }
139
+ # }
140
+ #
141
+ # class Book
142
+ # include MyVirtusModule
143
+ #
144
+ # attribute :published, Boolean
145
+ # end
146
+ #
147
+ # # This could be made more succinct as well
148
+ # class OtherBook
149
+ # include Virtus.module { |m| m.coerce = false }
150
+ # end
151
+ #
152
+ # @return [Module]
153
+ #
154
+ # @api public
155
+ def self.model(options = {}, &block)
156
+ ModelBuilder.call(options, &block)
157
+ end
158
+
159
+ # Builds a module for...modules
160
+ #
161
+ # @example
162
+ #
163
+ # module Common
164
+ # include Virtus.module
165
+ #
166
+ # attribute :name, String
167
+ # attribute :age, Integer
168
+ # end
169
+ #
170
+ # class User
171
+ # include Common
172
+ # end
173
+ #
174
+ # class Admin
175
+ # include Common
176
+ # end
177
+ #
178
+ # @return [Module]
179
+ #
180
+ # @api public
181
+ def self.module(options = {}, &block)
182
+ ModuleBuilder.call(options, &block)
183
+ end
184
+
185
+ # Builds a module for value object models
186
+ #
187
+ # @example
188
+ #
189
+ # class GeoLocation
190
+ # include Virtus.value_object
191
+ #
192
+ # values do
193
+ # attribute :lat, Float
194
+ # attribute :lng, Float
195
+ # end
196
+ # end
197
+ #
198
+ # @return [Module]
199
+ #
200
+ # @api public
201
+ def self.value_object(options = {}, &block)
202
+ ValueObjectBuilder.call(options, &block)
203
+ end
204
+
205
+ # Global configuration instance
206
+ #
207
+ # @ return [Configuration]
208
+ #
209
+ # @api private
210
+ def self.configuration
211
+ @configuration ||= Configuration.new
212
+ end
213
+
214
+ # @api private
215
+ def self.constantize(type)
216
+ inflector.constantize(type)
217
+ end
218
+
219
+ # @api private
220
+ def self.inflector
221
+ @inflector ||=
222
+ begin
223
+ require 'dry/inflector'
224
+ Dry::Inflector.new
225
+ rescue LoadError
226
+ raise(
227
+ NotImplementedError,
228
+ 'Virtus needs dry-inflector gem to constantize namespaced constant names'
229
+ )
230
+ end
231
+ end
232
+
233
+ # Finalize pending attributes
234
+ #
235
+ # @example
236
+ # class User
237
+ # include Virtus.model(:finalize => false)
238
+ #
239
+ # attribute :address, 'Address'
240
+ # end
241
+ #
242
+ # class Address
243
+ # include Virtus.model(:finalize => false)
244
+ #
245
+ # attribute :user, 'User'
246
+ # end
247
+ #
248
+ # Virtus.finalize # this will resolve constant names
249
+ #
250
+ # @return [Array] array of finalized models
251
+ #
252
+ # @api public
253
+ def self.finalize
254
+ Builder.pending.each do |klass|
255
+ klass.attribute_set.finalize
256
+ end
257
+ end
258
+
259
+ # @api private
260
+ def self.warn(msg)
261
+ Kernel.warn(msg)
262
+ end
263
+
264
+ end # module Virtus
265
+
266
+ require 'descendants_tracker'
267
+ require 'axiom-types'
268
+ require 'coercible'
269
+
270
+ require 'virtus/support/equalizer'
271
+ require 'virtus/support/options'
272
+ require 'virtus/support/type_lookup'
273
+
274
+ require 'virtus/model'
275
+ require 'virtus/extensions'
276
+ require 'virtus/const_missing_extensions'
277
+ require 'virtus/class_inclusions'
278
+ require 'virtus/module_extensions'
279
+
280
+ require 'virtus/configuration'
281
+ require 'virtus/builder'
282
+ require 'virtus/builder/hook_context'
283
+
284
+ require 'virtus/class_methods'
285
+ require 'virtus/instance_methods'
286
+
287
+ require 'virtus/value_object'
288
+
289
+ require 'virtus/coercer'
290
+ require 'virtus/attribute_set'
291
+
292
+ require 'virtus/attribute/default_value'
293
+ require 'virtus/attribute/default_value/from_clonable'
294
+ require 'virtus/attribute/default_value/from_callable'
295
+ require 'virtus/attribute/default_value/from_symbol'
296
+
297
+ require 'virtus/attribute'
298
+ require 'virtus/attribute/builder'
299
+ require 'virtus/attribute/coercer'
300
+ require 'virtus/attribute/accessor'
301
+ require 'virtus/attribute/coercible'
302
+ require 'virtus/attribute/strict'
303
+ require 'virtus/attribute/lazy_default'
304
+ require 'virtus/attribute/nullify_blank'
305
+
306
+ require 'virtus/attribute/boolean'
307
+ require 'virtus/attribute/collection'
308
+ require 'virtus/attribute/enum'
309
+ require 'virtus/attribute/hash'
310
+ require 'virtus/attribute/embedded_value'
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Adding attribute called 'attributes'" do
4
+
5
+ context "when mass assignment is disabled" do
6
+ before do
7
+ module Examples
8
+ class User
9
+ include Virtus.model(mass_assignment: false)
10
+
11
+ attribute :attributes
12
+ end
13
+ end
14
+ end
15
+
16
+ it "allows model to use `attributes` attribute" do
17
+ user = Examples::User.new
18
+ expect(user.attributes).to eq(nil)
19
+ user.attributes = "attributes string"
20
+ expect(user.attributes).to eq("attributes string")
21
+ end
22
+
23
+ it "doesn't accept `attributes` key in initializer" do
24
+ user = Examples::User.new(attributes: 'attributes string')
25
+ expect(user.attributes).to eq(nil)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,90 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'I can create a Virtus module' do
4
+ before do
5
+ module Examples
6
+ NoncoercingModule = Virtus.model { |config|
7
+ config.coerce = false
8
+ }
9
+
10
+ CoercingModule = Virtus.model { |config|
11
+ config.coerce = true
12
+
13
+ config.coercer do |coercer|
14
+ coercer.string.boolean_map = { 'yup' => true, 'nope' => false }
15
+ end
16
+ }
17
+
18
+ StrictModule = Virtus.model { |config|
19
+ config.strict = true
20
+ }
21
+
22
+ BlankModule = Virtus.model { |config|
23
+ config.nullify_blank = true
24
+ }
25
+
26
+ class NoncoercedUser
27
+ include NoncoercingModule
28
+
29
+ attribute :name, String
30
+ attribute :happy, String
31
+ end
32
+
33
+ class CoercedUser
34
+ include CoercingModule
35
+
36
+ attribute :name, String
37
+ attribute :happy, Boolean
38
+ end
39
+
40
+ class StrictModel
41
+ include StrictModule
42
+
43
+ attribute :stuff, Hash
44
+ attribute :happy, Boolean, :strict => false
45
+ end
46
+
47
+ class BlankModel
48
+ include BlankModule
49
+
50
+ attribute :stuff, Hash
51
+ attribute :happy, Boolean, :nullify_blank => false
52
+ end
53
+ end
54
+ end
55
+
56
+ specify 'including a custom module with coercion disabled' do
57
+ user = Examples::NoncoercedUser.new(:name => :Giorgio, :happy => 'yes')
58
+
59
+ expect(user.name).to be(:Giorgio)
60
+ expect(user.happy).to eql('yes')
61
+ end
62
+
63
+ specify 'including a custom module with coercion enabled' do
64
+ user = Examples::CoercedUser.new(:name => 'Paul', :happy => 'nope')
65
+
66
+ expect(user.name).to eql('Paul')
67
+ expect(user.happy).to be(false)
68
+ end
69
+
70
+ specify 'including a custom module with strict enabled' do
71
+ model = Examples::StrictModel.new
72
+
73
+ expect { model.stuff = 'foo' }.to raise_error(Virtus::CoercionError)
74
+
75
+ model.happy = 'foo'
76
+
77
+ expect(model.happy).to eql('foo')
78
+ end
79
+
80
+ specify 'including a custom module with nullify blank enabled' do
81
+ model = Examples::BlankModel.new
82
+
83
+ model.stuff = ''
84
+ expect(model.stuff).to be_nil
85
+
86
+ model.happy = 'foo'
87
+
88
+ expect(model.happy).to eql('foo')
89
+ end
90
+ end
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+
3
+ # TODO: refactor to make it inline with the new style of integration specs
4
+
5
+ class Address
6
+ include Virtus
7
+
8
+ attribute :address, String
9
+ attribute :locality, String
10
+ attribute :region, String
11
+ attribute :postal_code, String
12
+ end
13
+
14
+ class PhoneNumber
15
+ include Virtus
16
+
17
+ attribute :number, String
18
+ end
19
+
20
+ class User
21
+ include Virtus
22
+
23
+ attribute :phone_numbers, Array[PhoneNumber]
24
+ attribute :addresses, Set[Address]
25
+ end
26
+
27
+ describe User do
28
+ it { is_expected.to respond_to(:phone_numbers) }
29
+ it { is_expected.to respond_to(:phone_numbers=) }
30
+ it { is_expected.to respond_to(:addresses) }
31
+ it { is_expected.to respond_to(:addresses=) }
32
+
33
+ let(:instance) do
34
+ described_class.new(:phone_numbers => phone_numbers_attributes,
35
+ :addresses => addresses_attributes)
36
+ end
37
+
38
+ let(:phone_numbers_attributes) { [
39
+ { :number => '212-555-1212' },
40
+ { :number => '919-444-3265' },
41
+ ] }
42
+
43
+ let(:addresses_attributes) { [
44
+ { :address => '1234 Any St.', :locality => 'Anytown', :region => "DC", :postal_code => "21234" },
45
+ ] }
46
+
47
+ describe '#phone_numbers' do
48
+ describe 'first entry' do
49
+ subject { instance.phone_numbers.first }
50
+
51
+ it { is_expected.to be_instance_of(PhoneNumber) }
52
+
53
+ describe '#number' do
54
+ subject { super().number }
55
+ it { is_expected.to eql('212-555-1212') }
56
+ end
57
+ end
58
+
59
+ describe 'last entry' do
60
+ subject { instance.phone_numbers.last }
61
+
62
+ it { is_expected.to be_instance_of(PhoneNumber) }
63
+
64
+ describe '#number' do
65
+ subject { super().number }
66
+ it { is_expected.to eql('919-444-3265') }
67
+ end
68
+ end
69
+ end
70
+
71
+ describe '#addresses' do
72
+ subject { instance.addresses.first }
73
+
74
+ it { is_expected.to be_instance_of(Address) }
75
+
76
+ describe '#address' do
77
+ subject { super().address }
78
+ it { is_expected.to eql('1234 Any St.') }
79
+ end
80
+
81
+ describe '#locality' do
82
+ subject { super().locality }
83
+ it { is_expected.to eql('Anytown') }
84
+ end
85
+
86
+ describe '#region' do
87
+ subject { super().region }
88
+ it { is_expected.to eql('DC') }
89
+ end
90
+
91
+ describe '#postal_code' do
92
+ subject { super().postal_code }
93
+ it { is_expected.to eql('21234') }
94
+ end
95
+ end
96
+ end