virtus 0.5.2 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
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