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.
Files changed (52) hide show
  1. data/.rspec +2 -0
  2. data/.travis.yml +3 -0
  3. data/Changelog.md +9 -0
  4. data/Gemfile +2 -22
  5. data/Gemfile.devtools +44 -0
  6. data/README.md +78 -1
  7. data/Rakefile +3 -4
  8. data/config/flay.yml +2 -2
  9. data/config/mutant.yml +3 -0
  10. data/lib/virtus.rb +2 -1
  11. data/lib/virtus/attribute.rb +1 -1
  12. data/lib/virtus/attribute/array.rb +1 -0
  13. data/lib/virtus/attribute/class.rb +1 -1
  14. data/lib/virtus/attribute/hash.rb +84 -0
  15. data/lib/virtus/attribute/set.rb +1 -0
  16. data/lib/virtus/coercion/string.rb +6 -2
  17. data/lib/virtus/instance_methods.rb +8 -1
  18. data/lib/virtus/version.rb +1 -1
  19. data/spec/integration/default_values_spec.rb +22 -2
  20. data/spec/integration/hash_attributes_coercion_spec.rb +50 -0
  21. data/spec/shared/freeze_method_behavior.rb +1 -1
  22. data/spec/spec_helper.rb +6 -14
  23. data/spec/unit/virtus/attribute/hash/class_methods/merge_options_spec.rb +33 -0
  24. data/spec/unit/virtus/attribute/hash/coerce_spec.rb +20 -0
  25. data/spec/unit/virtus/attribute/hash/key_type_spec.rb +10 -0
  26. data/spec/unit/virtus/attribute/hash/value_type_spec.rb +10 -0
  27. data/spec/unit/virtus/coercion/string/class_methods/to_float_spec.rb +12 -0
  28. data/spec/unit/virtus/coercion/string/class_methods/to_integer_spec.rb +12 -0
  29. data/spec/unit/virtus/extensions/attribute_spec.rb +26 -0
  30. data/spec/unit/virtus/instance_methods/attributes_spec.rb +3 -1
  31. data/spec/unit/virtus/instance_methods/initialize_spec.rb +11 -5
  32. data/spec/unit/virtus/options/accept_options_spec.rb +1 -1
  33. data/spec/unit/virtus/options/accepted_options_spec.rb +1 -1
  34. data/spec/unit/virtus/options/options_spec.rb +1 -1
  35. data/spec/unit/virtus/type_lookup/determine_type_spec.rb +1 -1
  36. data/virtus.gemspec +4 -4
  37. metadata +76 -100
  38. data/lib/virtus/support/descendants_tracker.rb +0 -44
  39. data/spec/rcov.opts +0 -7
  40. data/spec/unit/virtus/attribute/object/class_methods/descendants_spec.rb +0 -22
  41. data/spec/unit/virtus/descendants_tracker/add_descendant_spec.rb +0 -24
  42. data/spec/unit/virtus/descendants_tracker/descendants_spec.rb +0 -22
  43. data/tasks/metrics/ci.rake +0 -7
  44. data/tasks/metrics/flay.rake +0 -41
  45. data/tasks/metrics/flog.rake +0 -43
  46. data/tasks/metrics/heckle.rake +0 -208
  47. data/tasks/metrics/metric_fu.rake +0 -29
  48. data/tasks/metrics/reek.rake +0 -9
  49. data/tasks/metrics/roodi.rake +0 -15
  50. data/tasks/metrics/yardstick.rake +0 -23
  51. data/tasks/spec.rake +0 -45
  52. data/tasks/yard.rake +0 -9
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --profile
@@ -11,6 +11,9 @@ rvm:
11
11
  - rbx-19mode
12
12
  - ree
13
13
  - jruby-head
14
+ matrix:
15
+ allow_failures:
16
+ - rvm: jruby-head
14
17
  notifications:
15
18
  email:
16
19
  - piotr.solnica@gmail.com
@@ -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
- group :metrics do
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
- platforms :mri_18 do
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')
@@ -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
- virtus
1
+ Virtus
2
2
  ======
3
3
 
4
4
  [![Build Status](https://secure.travis-ci.org/solnic/virtus.png)](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
@@ -1,6 +1,5 @@
1
- require 'rake'
1
+ # encoding: utf-8
2
2
 
3
- FileList['tasks/**/*.rake'].each { |task| import task }
3
+ require 'devtools'
4
4
 
5
- desc 'Default: run all specs'
6
- task :default => :spec
5
+ Devtools.init
@@ -1,3 +1,3 @@
1
1
  ---
2
- threshold: 19
3
- total_score: 425
2
+ threshold: 23
3
+ total_score: 499
@@ -0,0 +1,3 @@
1
+ ---
2
+ name: virtus
3
+ namespace: Virtus
@@ -47,7 +47,8 @@ module Virtus
47
47
 
48
48
  end # module Virtus
49
49
 
50
- require 'virtus/support/descendants_tracker'
50
+ require 'descendants_tracker'
51
+
51
52
  require 'virtus/support/type_lookup'
52
53
  require 'virtus/support/options'
53
54
  require 'virtus/support/equalizer'
@@ -83,7 +83,7 @@ module Virtus
83
83
  case class_or_name
84
84
  when ::Class
85
85
  Attribute::EmbeddedValue.determine_type(class_or_name) || super
86
- when ::Array, ::Set
86
+ when ::Array, ::Set, ::Hash
87
87
  super(class_or_name.class)
88
88
  else
89
89
  super
@@ -15,6 +15,7 @@ module Virtus
15
15
  class Array < Collection
16
16
  primitive ::Array
17
17
  coercion_method :to_array
18
+ default primitive.new
18
19
 
19
20
  include Collection::MemberCoercion
20
21
 
@@ -7,7 +7,7 @@ module Virtus
7
7
  # class Entity
8
8
  # include Virtus
9
9
  #
10
- # attribute :type, Class
10
+ # attribute :model, Class
11
11
  # end
12
12
  #
13
13
  # post = Entity.new(:model => Model)
@@ -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
@@ -15,6 +15,7 @@ module Virtus
15
15
  class Set < Collection
16
16
  primitive ::Set
17
17
  coercion_method :to_set
18
+ default primitive.new
18
19
 
19
20
  include Collection::MemberCoercion
20
21
 
@@ -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+#{EXPONENT_REGEXP}?)/.freeze
15
- NUMERIC_REGEXP = /\A(#{INTEGER_REGEXP}#{FRACTIONAL_REGEXP}?|#{FRACTIONAL_REGEXP})\z/.freeze
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).each do |name, value|
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
@@ -1,3 +1,3 @@
1
1
  module Virtus
2
- VERSION = '0.5.2'
2
+ VERSION = '0.5.3'
3
3
  end
@@ -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 'with a ValueObject' do
53
- it 'should not duplicate the ValueObject' do
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