virtus 0.5.2 → 0.5.3
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/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Changelog.md +9 -0
- data/Gemfile +2 -22
- data/Gemfile.devtools +44 -0
- data/README.md +78 -1
- data/Rakefile +3 -4
- data/config/flay.yml +2 -2
- data/config/mutant.yml +3 -0
- data/lib/virtus.rb +2 -1
- data/lib/virtus/attribute.rb +1 -1
- data/lib/virtus/attribute/array.rb +1 -0
- data/lib/virtus/attribute/class.rb +1 -1
- data/lib/virtus/attribute/hash.rb +84 -0
- data/lib/virtus/attribute/set.rb +1 -0
- data/lib/virtus/coercion/string.rb +6 -2
- data/lib/virtus/instance_methods.rb +8 -1
- data/lib/virtus/version.rb +1 -1
- data/spec/integration/default_values_spec.rb +22 -2
- data/spec/integration/hash_attributes_coercion_spec.rb +50 -0
- data/spec/shared/freeze_method_behavior.rb +1 -1
- data/spec/spec_helper.rb +6 -14
- data/spec/unit/virtus/attribute/hash/class_methods/merge_options_spec.rb +33 -0
- data/spec/unit/virtus/attribute/hash/coerce_spec.rb +20 -0
- data/spec/unit/virtus/attribute/hash/key_type_spec.rb +10 -0
- data/spec/unit/virtus/attribute/hash/value_type_spec.rb +10 -0
- data/spec/unit/virtus/coercion/string/class_methods/to_float_spec.rb +12 -0
- data/spec/unit/virtus/coercion/string/class_methods/to_integer_spec.rb +12 -0
- data/spec/unit/virtus/extensions/attribute_spec.rb +26 -0
- data/spec/unit/virtus/instance_methods/attributes_spec.rb +3 -1
- data/spec/unit/virtus/instance_methods/initialize_spec.rb +11 -5
- data/spec/unit/virtus/options/accept_options_spec.rb +1 -1
- data/spec/unit/virtus/options/accepted_options_spec.rb +1 -1
- data/spec/unit/virtus/options/options_spec.rb +1 -1
- data/spec/unit/virtus/type_lookup/determine_type_spec.rb +1 -1
- data/virtus.gemspec +4 -4
- metadata +76 -100
- data/lib/virtus/support/descendants_tracker.rb +0 -44
- data/spec/rcov.opts +0 -7
- data/spec/unit/virtus/attribute/object/class_methods/descendants_spec.rb +0 -22
- data/spec/unit/virtus/descendants_tracker/add_descendant_spec.rb +0 -24
- data/spec/unit/virtus/descendants_tracker/descendants_spec.rb +0 -22
- data/tasks/metrics/ci.rake +0 -7
- data/tasks/metrics/flay.rake +0 -41
- data/tasks/metrics/flog.rake +0 -43
- data/tasks/metrics/heckle.rake +0 -208
- data/tasks/metrics/metric_fu.rake +0 -29
- data/tasks/metrics/reek.rake +0 -9
- data/tasks/metrics/roodi.rake +0 -15
- data/tasks/metrics/yardstick.rake +0 -23
- data/tasks/spec.rake +0 -45
- data/tasks/yard.rake +0 -9
data/.rspec
ADDED
data/.travis.yml
CHANGED
data/Changelog.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
# v0.5.3 2012-09-01 2012-12-13
|
2
|
+
|
3
|
+
* [feature] Added Hash member type coercion [example](https://github.com/solnic/virtus#hash-attributes-coercion) (greyblake)
|
4
|
+
* [fixed] Fixed issues with String=>Integer coercion and e-notation (greyblake)
|
5
|
+
* [changed] Replaced internal DescendantsTracker with the extracted gem (solnic)
|
6
|
+
* [interal] Switched to rspec 2 and mutant for mutation testing (mbj)
|
7
|
+
|
8
|
+
[Compare v0.5.2..v0.5.3](https://github.com/solnic/virtus/compare/v0.5.2...v0.5.3)
|
9
|
+
|
1
10
|
# v0.5.2 2012-09-01
|
2
11
|
|
3
12
|
* [feature] Object is now the default attribute type (dkubb)
|
data/Gemfile
CHANGED
@@ -2,26 +2,6 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
gemspec
|
4
4
|
|
5
|
-
|
6
|
-
gem 'fattr', '~> 2.2.0'
|
7
|
-
gem 'arrayfields', '~> 4.7.4'
|
8
|
-
gem 'flay', '~> 1.4.2'
|
9
|
-
gem 'flog', '~> 2.5.1'
|
10
|
-
gem 'map', '~> 5.2.0'
|
11
|
-
gem 'reek', '~> 1.2.8', :git => 'git://github.com/dkubb/reek.git'
|
12
|
-
gem 'roodi', '~> 2.1.0'
|
13
|
-
gem 'yardstick', '~> 0.4.0'
|
5
|
+
gem 'devtools', :git => 'https://github.com/datamapper/devtools'
|
14
6
|
|
15
|
-
|
16
|
-
gem 'heckle', '~> 1.4.3'
|
17
|
-
gem 'json', '~> 1.6.4'
|
18
|
-
gem 'metric_fu', '~> 2.1.1'
|
19
|
-
gem 'mspec', '~> 1.5.17'
|
20
|
-
gem 'rcov', '~> 0.9.9'
|
21
|
-
gem 'ruby2ruby', '= 1.2.2'
|
22
|
-
end
|
23
|
-
|
24
|
-
platforms :rbx do
|
25
|
-
gem 'pelusa', :git => 'https://github.com/codegram/pelusa.git'
|
26
|
-
end
|
27
|
-
end
|
7
|
+
eval File.read('Gemfile.devtools')
|
data/Gemfile.devtools
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
group :development do
|
2
|
+
gem 'rake', '~> 0.9.2'
|
3
|
+
gem 'rspec', '~> 2.12.0'
|
4
|
+
gem 'yard', '~> 0.8.3'
|
5
|
+
end
|
6
|
+
|
7
|
+
group :guard do
|
8
|
+
gem 'guard', '~> 1.5.4'
|
9
|
+
gem 'guard-bundler', '~> 1.0.0'
|
10
|
+
gem 'guard-rspec', '~> 2.1.1'
|
11
|
+
gem 'rb-inotify', :git => 'https://github.com/mbj/rb-inotify'
|
12
|
+
end
|
13
|
+
|
14
|
+
group :benchmarks do
|
15
|
+
gem 'rbench', '~> 0.2.3'
|
16
|
+
end
|
17
|
+
|
18
|
+
platform :jruby do
|
19
|
+
group :jruby do
|
20
|
+
gem 'jruby-openssl', '~> 0.7.4'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
group :metrics do
|
25
|
+
gem 'flay', '~> 1.4.2'
|
26
|
+
gem 'flog', '~> 2.5.1'
|
27
|
+
gem 'reek', '~> 1.2.8', :git => 'https://github.com/dkubb/reek.git'
|
28
|
+
gem 'roodi', '~> 2.1.0'
|
29
|
+
gem 'yardstick', '~> 0.8.0'
|
30
|
+
gem 'mutant', '~> 0.2.0'
|
31
|
+
|
32
|
+
platforms :ruby_18, :ruby_19 do
|
33
|
+
# this indirectly depends on ffi which does not build on ruby-head
|
34
|
+
gem 'yard-spellcheck', '~> 0.1.5'
|
35
|
+
end
|
36
|
+
|
37
|
+
platforms :mri_19 do
|
38
|
+
gem 'simplecov', '~> 0.7'
|
39
|
+
end
|
40
|
+
|
41
|
+
platforms :rbx do
|
42
|
+
gem 'pelusa', '~> 0.2.1'
|
43
|
+
end
|
44
|
+
end
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
Virtus
|
2
2
|
======
|
3
3
|
|
4
4
|
[](http://travis-ci.org/solnic/virtus)
|
@@ -51,6 +51,11 @@ user.age = '28' # => 28
|
|
51
51
|
user.age.class # => Fixnum
|
52
52
|
|
53
53
|
user.birthday = 'November 18th, 1983' # => #<DateTime: 1983-11-18T00:00:00+00:00 (4891313/2,0/1,2299161)>
|
54
|
+
|
55
|
+
# mass-assignment
|
56
|
+
user.attributes = { :name => 'Jane', :age => 21 }
|
57
|
+
user.name # => "Jane"
|
58
|
+
user.age # => 21
|
54
59
|
```
|
55
60
|
|
56
61
|
### Using Virtus with Modules
|
@@ -205,6 +210,78 @@ user.phone_numbers # => [#<PhoneNumber:0x007fdb2d3bef88 @number="212-555-1212">,
|
|
205
210
|
user.addresses # => #<Set: {#<Address:0x007fdb2d3be448 @address="1234 Any St.", @locality="Anytown", @region="DC", @postal_code="21234">}>
|
206
211
|
```
|
207
212
|
|
213
|
+
### Hash attributes coercion
|
214
|
+
|
215
|
+
``` ruby
|
216
|
+
class Package
|
217
|
+
include Virtus
|
218
|
+
|
219
|
+
attribute :dimensions, Hash[Symbol => Float]
|
220
|
+
end
|
221
|
+
|
222
|
+
package = Package.new(:dimensions => { 'width' => "2.2", :height => 2, "length" => 4.5 })
|
223
|
+
package.dimensions # => { :width => 2.2, :height => 2.0, :length => 4.5 }
|
224
|
+
```
|
225
|
+
|
226
|
+
### IMPORTANT note about member coercions
|
227
|
+
|
228
|
+
Virtus performs coercions only when a value is being assigned. If you mutate the value later on using its own
|
229
|
+
interfaces then coercion won't be triggered.
|
230
|
+
|
231
|
+
Here's an example:
|
232
|
+
|
233
|
+
``` ruby
|
234
|
+
class Book
|
235
|
+
include Virtus
|
236
|
+
|
237
|
+
attribute :title, String
|
238
|
+
end
|
239
|
+
|
240
|
+
class Library
|
241
|
+
include Virtus
|
242
|
+
|
243
|
+
attribute :books, Array[Book]
|
244
|
+
end
|
245
|
+
|
246
|
+
library = Library.new
|
247
|
+
|
248
|
+
# This will coerce Hash to a Book instance
|
249
|
+
library.books = [ { :title => 'Introduction to Virtus' } ]
|
250
|
+
|
251
|
+
# This WILL NOT COERCE the value because you mutate the books array with Array#<<
|
252
|
+
library.books << { :title => 'Another Introduction to Virtus' }
|
253
|
+
```
|
254
|
+
|
255
|
+
A suggested solution to this problem would be to introduce your own class instead of using Array and implement
|
256
|
+
mutation methods that perform coercions. For example:
|
257
|
+
|
258
|
+
``` ruby
|
259
|
+
class Book
|
260
|
+
include Virtus
|
261
|
+
|
262
|
+
attribute :title, String
|
263
|
+
end
|
264
|
+
|
265
|
+
class BookCollection < Array
|
266
|
+
def <<(book)
|
267
|
+
if book.kind_of?(Hash)
|
268
|
+
super(Book.new(book))
|
269
|
+
else
|
270
|
+
super
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
class Library
|
276
|
+
include Virtus
|
277
|
+
|
278
|
+
attribute :books, BookCollection[Book]
|
279
|
+
end
|
280
|
+
|
281
|
+
library = Library.new
|
282
|
+
library.books << { :title => 'Another Introduction to Virtus' }
|
283
|
+
```
|
284
|
+
|
208
285
|
### Value Objects
|
209
286
|
|
210
287
|
``` ruby
|
data/Rakefile
CHANGED
data/config/flay.yml
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
---
|
2
|
-
threshold:
|
3
|
-
total_score:
|
2
|
+
threshold: 23
|
3
|
+
total_score: 499
|
data/config/mutant.yml
ADDED
data/lib/virtus.rb
CHANGED
data/lib/virtus/attribute.rb
CHANGED
@@ -15,6 +15,90 @@ module Virtus
|
|
15
15
|
class Hash < Object
|
16
16
|
primitive ::Hash
|
17
17
|
coercion_method :to_hash
|
18
|
+
default primitive.new
|
19
|
+
|
20
|
+
# The type to which keys of this hash will be coerced
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
#
|
24
|
+
# class Request
|
25
|
+
# include Virtus
|
26
|
+
#
|
27
|
+
# attribute :headers, Hash[Symbol => String]
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# Post.attributes[:headers].key_type # => Virtus::Attribute::Symbol
|
31
|
+
#
|
32
|
+
# @return [Virtus::Attribute]
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
attr_reader :key_type
|
36
|
+
|
37
|
+
# The type to which values of this hash will be coerced
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
#
|
41
|
+
# class Request
|
42
|
+
# include Virtus
|
43
|
+
#
|
44
|
+
# attribute :headers, Hash[Symbol => String]
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# Post.attributes[:headers].value_type # => Virtus::Attribute::String
|
48
|
+
#
|
49
|
+
# @return [Virtus::Attribute]
|
50
|
+
#
|
51
|
+
# @api public
|
52
|
+
attr_reader :value_type
|
53
|
+
|
54
|
+
# Handles hashes with [key_type => value_type] syntax.
|
55
|
+
#
|
56
|
+
# @param [Class]
|
57
|
+
#
|
58
|
+
# @param [Hash]
|
59
|
+
#
|
60
|
+
# @return [Hash]
|
61
|
+
#
|
62
|
+
# @api private
|
63
|
+
def self.merge_options(type, options)
|
64
|
+
if !type.respond_to?(:size)
|
65
|
+
options
|
66
|
+
elsif type.size > 1
|
67
|
+
raise ArgumentError, "more than one [key => value] pair in `#{type.inspect}`"
|
68
|
+
else
|
69
|
+
options.merge(:key_type => type.keys.first, :value_type => type.values.first)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Initializes an instance of {Virtus::Attribute::Hash}
|
74
|
+
#
|
75
|
+
# @api private
|
76
|
+
def initialize(*)
|
77
|
+
super
|
78
|
+
if @options.has_key?(:key_type) && @options.has_key?(:value_type)
|
79
|
+
@key_type = @options[:key_type]
|
80
|
+
@value_type = @options[:value_type]
|
81
|
+
@key_type_instance = Attribute.build(@name, @key_type)
|
82
|
+
@value_type_instance = Attribute.build(@name, @value_type)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Coerce a hash with keys and values
|
87
|
+
#
|
88
|
+
# @param [Object]
|
89
|
+
#
|
90
|
+
# @return [Object]
|
91
|
+
#
|
92
|
+
# @api private
|
93
|
+
def coerce(value)
|
94
|
+
coerced = super
|
95
|
+
return coerced unless coerced.respond_to?(:each_with_object)
|
96
|
+
coerced.each_with_object({}) do |key_and_value, hash|
|
97
|
+
key = @key_type_instance.coerce(key_and_value[0])
|
98
|
+
value = @value_type_instance.coerce(key_and_value[1])
|
99
|
+
hash[key] = value
|
100
|
+
end
|
101
|
+
end
|
18
102
|
|
19
103
|
end # class Hash
|
20
104
|
end # class Attribute
|
data/lib/virtus/attribute/set.rb
CHANGED
@@ -11,8 +11,12 @@ module Virtus
|
|
11
11
|
|
12
12
|
INTEGER_REGEXP = /[-+]?(?:0|[1-9]\d*)/.freeze
|
13
13
|
EXPONENT_REGEXP = /(?:[eE][-+]?\d+)/.freeze
|
14
|
-
FRACTIONAL_REGEXP = /(?:\.\d
|
15
|
-
|
14
|
+
FRACTIONAL_REGEXP = /(?:\.\d+)/.freeze
|
15
|
+
|
16
|
+
NUMERIC_REGEXP = /\A(
|
17
|
+
#{INTEGER_REGEXP}#{FRACTIONAL_REGEXP}?#{EXPONENT_REGEXP}? |
|
18
|
+
#{FRACTIONAL_REGEXP}#{EXPONENT_REGEXP}?
|
19
|
+
)\z/x.freeze
|
16
20
|
|
17
21
|
# Coerce give value to a constant
|
18
22
|
#
|
@@ -191,7 +191,14 @@ module Virtus
|
|
191
191
|
#
|
192
192
|
# @api private
|
193
193
|
def set_attributes(attributes)
|
194
|
-
::Hash.try_convert(attributes)
|
194
|
+
hash = ::Hash.try_convert(attributes)
|
195
|
+
|
196
|
+
if hash.nil?
|
197
|
+
raise NoMethodError,
|
198
|
+
"Expected #{attributes.inspect} to respond to #to_hash"
|
199
|
+
end
|
200
|
+
|
201
|
+
hash.each do |name, value|
|
195
202
|
set_attribute(name, value) if allowed_writer_methods.include?("#{name}=")
|
196
203
|
end
|
197
204
|
end
|
data/lib/virtus/version.rb
CHANGED
@@ -20,6 +20,9 @@ describe "default values" do
|
|
20
20
|
attribute :published, Boolean, :default => false, :accessor => :private
|
21
21
|
attribute :editor_title, String, :default => :default_editor_title
|
22
22
|
attribute :reference, String, :default => Reference.new
|
23
|
+
attribute :revisions, Array
|
24
|
+
attribute :index, Hash
|
25
|
+
attribute :authors, Set
|
23
26
|
|
24
27
|
def default_editor_title
|
25
28
|
published? ? title : "UNPUBLISHED: #{title}"
|
@@ -49,12 +52,29 @@ describe "default values" do
|
|
49
52
|
subject.editor_title.should == 'UNPUBLISHED: Top Secret'
|
50
53
|
end
|
51
54
|
|
52
|
-
context '
|
53
|
-
it '
|
55
|
+
context 'a ValueObject' do
|
56
|
+
it 'does not duplicate the ValueObject' do
|
54
57
|
page1 = Examples::Page.new
|
55
58
|
page2 = Examples::Page.new
|
56
59
|
page1.reference.should equal(page2.reference)
|
57
60
|
end
|
58
61
|
end
|
59
62
|
|
63
|
+
context 'an Array' do
|
64
|
+
specify 'without a default the value is an empty Array' do
|
65
|
+
subject.revisions.should eql([])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'a Hash' do
|
70
|
+
specify 'without a default the value is an empty Hash' do
|
71
|
+
subject.index.should eql({})
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'a Set' do
|
76
|
+
specify 'without a default the value is an empty Set' do
|
77
|
+
subject.authors.should eql(Set.new)
|
78
|
+
end
|
79
|
+
end
|
60
80
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
|
4
|
+
class Package
|
5
|
+
include Virtus
|
6
|
+
|
7
|
+
attribute :dimensions, Hash[Symbol => Float]
|
8
|
+
attribute :meta_info , Hash[String => String]
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
describe Package do
|
13
|
+
let(:instance) do
|
14
|
+
described_class.new(
|
15
|
+
:dimensions => { 'width' => "2.2", :height => 2, "length" => 4.5 },
|
16
|
+
:meta_info => { 'from' => :Me , :to => 'You' }
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:dimensions) { instance.dimensions }
|
21
|
+
let(:meta_info) { instance.meta_info }
|
22
|
+
|
23
|
+
describe '#dimensions' do
|
24
|
+
subject { dimensions }
|
25
|
+
|
26
|
+
it { should have(3).keys }
|
27
|
+
it { should have_key :width }
|
28
|
+
it { should have_key :height }
|
29
|
+
it { should have_key :length }
|
30
|
+
|
31
|
+
it 'should be coerced to [Symbol => Float] format' do
|
32
|
+
dimensions[:width].should be_eql(2.2)
|
33
|
+
dimensions[:height].should be_eql(2.0)
|
34
|
+
dimensions[:length].should be_eql(4.5)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#meta_info' do
|
39
|
+
subject { meta_info }
|
40
|
+
|
41
|
+
it { should have(2).keys }
|
42
|
+
it { should have_key 'from' }
|
43
|
+
it { should have_key 'to' }
|
44
|
+
|
45
|
+
it 'should be coerced to [String => String] format' do
|
46
|
+
meta_info['from'].should == 'Me'
|
47
|
+
meta_info['to'].should == 'You'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|