virtus 1.0.1 → 2.0.0

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 (99) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +19 -15
  3. data/Changelog.md +43 -2
  4. data/Gemfile +5 -5
  5. data/README.md +113 -78
  6. data/Rakefile +13 -3
  7. data/lib/virtus.rb +46 -6
  8. data/lib/virtus/attribute.rb +21 -3
  9. data/lib/virtus/attribute/accessor.rb +11 -0
  10. data/lib/virtus/attribute/builder.rb +8 -13
  11. data/lib/virtus/attribute/collection.rb +12 -3
  12. data/lib/virtus/attribute/default_value.rb +2 -0
  13. data/lib/virtus/attribute/hash.rb +3 -3
  14. data/lib/virtus/attribute/nullify_blank.rb +24 -0
  15. data/lib/virtus/attribute/strict.rb +1 -1
  16. data/lib/virtus/attribute_set.rb +2 -2
  17. data/lib/virtus/builder.rb +2 -6
  18. data/lib/virtus/class_inclusions.rb +0 -1
  19. data/lib/virtus/coercer.rb +1 -0
  20. data/lib/virtus/configuration.rb +17 -36
  21. data/lib/virtus/extensions.rb +13 -21
  22. data/lib/virtus/instance_methods.rb +3 -2
  23. data/lib/virtus/model.rb +1 -3
  24. data/lib/virtus/module_extensions.rb +8 -2
  25. data/lib/virtus/support/equalizer.rb +1 -1
  26. data/lib/virtus/support/options.rb +2 -1
  27. data/lib/virtus/support/type_lookup.rb +1 -1
  28. data/lib/virtus/version.rb +1 -1
  29. data/spec/integration/attributes_attribute_spec.rb +28 -0
  30. data/spec/integration/building_module_spec.rb +22 -0
  31. data/spec/integration/collection_member_coercion_spec.rb +34 -13
  32. data/spec/integration/custom_attributes_spec.rb +2 -2
  33. data/spec/integration/custom_collection_attributes_spec.rb +6 -6
  34. data/spec/integration/default_values_spec.rb +8 -8
  35. data/spec/integration/defining_attributes_spec.rb +25 -18
  36. data/spec/integration/embedded_value_spec.rb +5 -5
  37. data/spec/integration/extending_objects_spec.rb +5 -5
  38. data/spec/integration/hash_attributes_coercion_spec.rb +16 -12
  39. data/spec/integration/mass_assignment_with_accessors_spec.rb +5 -5
  40. data/spec/integration/overriding_virtus_spec.rb +4 -4
  41. data/spec/integration/required_attributes_spec.rb +1 -1
  42. data/spec/integration/struct_as_embedded_value_spec.rb +4 -4
  43. data/spec/integration/using_modules_spec.rb +8 -8
  44. data/spec/integration/value_object_with_custom_constructor_spec.rb +4 -4
  45. data/spec/integration/virtus/instance_level_attributes_spec.rb +1 -1
  46. data/spec/integration/virtus/value_object_spec.rb +14 -14
  47. data/spec/shared/freeze_method_behavior.rb +6 -3
  48. data/spec/shared/idempotent_method_behaviour.rb +1 -1
  49. data/spec/shared/options_class_method.rb +3 -3
  50. data/spec/spec_helper.rb +2 -18
  51. data/spec/unit/virtus/attribute/boolean/coerce_spec.rb +3 -3
  52. data/spec/unit/virtus/attribute/boolean/value_coerced_predicate_spec.rb +3 -3
  53. data/spec/unit/virtus/attribute/class_methods/build_spec.rb +64 -24
  54. data/spec/unit/virtus/attribute/class_methods/coerce_spec.rb +2 -2
  55. data/spec/unit/virtus/attribute/coerce_spec.rb +58 -10
  56. data/spec/unit/virtus/attribute/coercible_predicate_spec.rb +2 -2
  57. data/spec/unit/virtus/attribute/collection/class_methods/build_spec.rb +15 -4
  58. data/spec/unit/virtus/attribute/collection/coerce_spec.rb +25 -4
  59. data/spec/unit/virtus/attribute/collection/value_coerced_predicate_spec.rb +31 -0
  60. data/spec/unit/virtus/attribute/comparison_spec.rb +20 -0
  61. data/spec/unit/virtus/attribute/custom_collection_spec.rb +8 -2
  62. data/spec/unit/virtus/attribute/defined_spec.rb +20 -0
  63. data/spec/unit/virtus/attribute/embedded_value/class_methods/build_spec.rb +30 -15
  64. data/spec/unit/virtus/attribute/embedded_value/coerce_spec.rb +25 -11
  65. data/spec/unit/virtus/attribute/get_spec.rb +2 -2
  66. data/spec/unit/virtus/attribute/hash/class_methods/build_spec.rb +21 -9
  67. data/spec/unit/virtus/attribute/hash/coerce_spec.rb +9 -9
  68. data/spec/unit/virtus/attribute/lazy_predicate_spec.rb +2 -2
  69. data/spec/unit/virtus/attribute/rename_spec.rb +6 -3
  70. data/spec/unit/virtus/attribute/required_predicate_spec.rb +2 -2
  71. data/spec/unit/virtus/attribute/set_default_value_spec.rb +43 -10
  72. data/spec/unit/virtus/attribute/set_spec.rb +1 -1
  73. data/spec/unit/virtus/attribute/value_coerced_predicate_spec.rb +2 -2
  74. data/spec/unit/virtus/attribute_set/append_spec.rb +6 -6
  75. data/spec/unit/virtus/attribute_set/define_reader_method_spec.rb +12 -11
  76. data/spec/unit/virtus/attribute_set/define_writer_method_spec.rb +13 -12
  77. data/spec/unit/virtus/attribute_set/each_spec.rb +21 -16
  78. data/spec/unit/virtus/attribute_set/element_reference_spec.rb +2 -2
  79. data/spec/unit/virtus/attribute_set/element_set_spec.rb +17 -9
  80. data/spec/unit/virtus/attribute_set/merge_spec.rb +7 -5
  81. data/spec/unit/virtus/attribute_set/reset_spec.rb +22 -11
  82. data/spec/unit/virtus/attribute_spec.rb +8 -7
  83. data/spec/unit/virtus/attributes_reader_spec.rb +1 -1
  84. data/spec/unit/virtus/attributes_writer_spec.rb +1 -1
  85. data/spec/unit/virtus/element_reader_spec.rb +1 -1
  86. data/spec/unit/virtus/freeze_spec.rb +23 -3
  87. data/spec/unit/virtus/model_spec.rb +38 -7
  88. data/spec/unit/virtus/module_spec.rb +59 -2
  89. data/spec/unit/virtus/set_default_attributes_spec.rb +10 -3
  90. data/spec/unit/virtus/value_object_spec.rb +15 -5
  91. data/virtus.gemspec +7 -5
  92. metadata +46 -44
  93. data/.ruby-version +0 -1
  94. data/Gemfile.devtools +0 -54
  95. data/config/flay.yml +0 -3
  96. data/config/flog.yml +0 -2
  97. data/config/mutant.yml +0 -15
  98. data/config/reek.yml +0 -146
  99. data/config/yardstick.yml +0 -2
@@ -15,7 +15,9 @@ module Virtus
15
15
  # @api private
16
16
  def self.setup(mod, inclusions = [Model], attribute_definitions = [])
17
17
  mod.instance_variable_set('@inclusions', inclusions)
18
- mod.instance_variable_set('@attribute_definitions', attribute_definitions)
18
+ existing_attributes = mod.instance_variable_get('@attribute_definitions')
19
+ new_attributes = (existing_attributes || []) + attribute_definitions
20
+ mod.instance_variable_set('@attribute_definitions', new_attributes)
19
21
  end
20
22
 
21
23
  # Define an attribute in the module
@@ -57,7 +59,11 @@ module Virtus
57
59
  super
58
60
 
59
61
  if Class === object
60
- @inclusions.each { |mod| object.send(:include, mod) }
62
+ @inclusions.reject do |mod|
63
+ object.ancestors.include?(mod)
64
+ end.each do |mod|
65
+ object.send(:include, mod)
66
+ end
61
67
  define_attributes(object)
62
68
  else
63
69
  object.extend(ModuleExtensions)
@@ -65,7 +65,7 @@ module Virtus
65
65
  def define_hash_method
66
66
  keys = @keys
67
67
  define_method(:hash) do
68
- keys.map { |key| send(key).hash }.reduce(self.class.hash, :^)
68
+ keys.map { |key| send(key) }.push(self.class).hash
69
69
  end
70
70
  end
71
71
 
@@ -37,7 +37,7 @@ module Virtus
37
37
  # Defines which options are valid for a given attribute class
38
38
  #
39
39
  # @example
40
- # class MyAttribute < Virtus::Attribute::Object
40
+ # class MyAttribute < Virtus::Attribute
41
41
  # accept_options :foo, :bar
42
42
  # end
43
43
  #
@@ -61,6 +61,7 @@ module Virtus
61
61
  def define_option_method(option)
62
62
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
63
63
  def self.#{option}(value = Undefined) # def self.primitive(value = Undefined)
64
+ @#{option} = nil unless defined?(@#{option}) # @primitive = nil unless defined?(@primitive)
64
65
  return @#{option} if value.equal?(Undefined) # return @primitive if value.equal?(Undefined)
65
66
  @#{option} = value # @primitive = value
66
67
  self # self
@@ -48,7 +48,7 @@ module Virtus
48
48
 
49
49
  # @api private
50
50
  def determine_type_and_cache(class_or_name)
51
- type = case class_or_name
51
+ case class_or_name
52
52
  when singleton_class
53
53
  determine_type_from_descendant(class_or_name)
54
54
  when Class
@@ -1,3 +1,3 @@
1
1
  module Virtus
2
- VERSION = '1.0.1'
2
+ VERSION = '2.0.0'.freeze
3
3
  end
@@ -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
@@ -19,6 +19,10 @@ describe 'I can create a Virtus module' do
19
19
  config.strict = true
20
20
  }
21
21
 
22
+ BlankModule = Virtus.model { |config|
23
+ config.nullify_blank = true
24
+ }
25
+
22
26
  class NoncoercedUser
23
27
  include NoncoercingModule
24
28
 
@@ -39,6 +43,13 @@ describe 'I can create a Virtus module' do
39
43
  attribute :stuff, Hash
40
44
  attribute :happy, Boolean, :strict => false
41
45
  end
46
+
47
+ class BlankModel
48
+ include BlankModule
49
+
50
+ attribute :stuff, Hash
51
+ attribute :happy, Boolean, :nullify_blank => false
52
+ end
42
53
  end
43
54
  end
44
55
 
@@ -65,4 +76,15 @@ describe 'I can create a Virtus module' do
65
76
 
66
77
  expect(model.happy).to eql('foo')
67
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
68
90
  end
@@ -25,10 +25,10 @@ class User
25
25
  end
26
26
 
27
27
  describe User do
28
- it { should respond_to(:phone_numbers) }
29
- it { should respond_to(:phone_numbers=) }
30
- it { should respond_to(:addresses) }
31
- it { should respond_to(:addresses=) }
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
32
 
33
33
  let(:instance) do
34
34
  described_class.new(:phone_numbers => phone_numbers_attributes,
@@ -48,28 +48,49 @@ describe User do
48
48
  describe 'first entry' do
49
49
  subject { instance.phone_numbers.first }
50
50
 
51
- it { should be_instance_of(PhoneNumber) }
51
+ it { is_expected.to be_instance_of(PhoneNumber) }
52
52
 
53
- its(:number) { should eql('212-555-1212') }
53
+ describe '#number' do
54
+ subject { super().number }
55
+ it { is_expected.to eql('212-555-1212') }
56
+ end
54
57
  end
55
58
 
56
59
  describe 'last entry' do
57
60
  subject { instance.phone_numbers.last }
58
61
 
59
- it { should be_instance_of(PhoneNumber) }
62
+ it { is_expected.to be_instance_of(PhoneNumber) }
60
63
 
61
- its(:number) { should eql('919-444-3265') }
64
+ describe '#number' do
65
+ subject { super().number }
66
+ it { is_expected.to eql('919-444-3265') }
67
+ end
62
68
  end
63
69
  end
64
70
 
65
71
  describe '#addresses' do
66
72
  subject { instance.addresses.first }
67
73
 
68
- it { should be_instance_of(Address) }
74
+ it { is_expected.to be_instance_of(Address) }
69
75
 
70
- its(:address) { should eql('1234 Any St.') }
71
- its(:locality) { should eql('Anytown') }
72
- its(:region) { should eql('DC') }
73
- its(:postal_code) { should eql('21234') }
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
74
95
  end
75
96
  end
@@ -31,12 +31,12 @@ describe 'custom attributes' do
31
31
  specify 'allows you to define custom attributes' do
32
32
  regexp = /awesome/
33
33
  subject.expression = regexp
34
- subject.expression.should == regexp
34
+ expect(subject.expression).to eq(regexp)
35
35
  end
36
36
 
37
37
  specify 'allows you to define coercion methods' do
38
38
  subject.scream = 'welcome'
39
- subject.scream.should == 'WELCOME'
39
+ expect(subject.scream).to eq('WELCOME')
40
40
  end
41
41
 
42
42
  end
@@ -36,7 +36,7 @@ describe 'custom collection attributes' do
36
36
  shared_examples_for 'a collection' do
37
37
  it 'can be used as Virtus attributes' do
38
38
  attribute = Examples::Library.attribute_set[:books]
39
- attribute.should be_kind_of(Examples::BookCollectionAttribute)
39
+ expect(attribute).to be_kind_of(Examples::BookCollectionAttribute)
40
40
  end
41
41
 
42
42
  it 'defaults to an empty collection' do
@@ -55,18 +55,18 @@ describe 'custom collection attributes' do
55
55
 
56
56
  it 'coerces an array of attribute hashes' do
57
57
  library.books = [{ :title => 'Foo' }]
58
- books.should be_kind_of(Examples::BookCollection)
58
+ expect(books).to be_kind_of(Examples::BookCollection)
59
59
  end
60
60
 
61
61
  it 'coerces its members' do
62
62
  library.books = [{ :title => 'Foo' }]
63
- books.count.should == 1
64
- books.first.should be_kind_of(Examples::Book)
63
+ expect(books.count).to eq(1)
64
+ expect(books.first).to be_kind_of(Examples::Book)
65
65
  end
66
66
 
67
67
  def books_should_be_an_empty_collection
68
- books.should be_kind_of(Examples::BookCollection)
69
- books.count.should == 0
68
+ expect(books).to be_kind_of(Examples::BookCollection)
69
+ expect(books.count).to eq(0)
70
70
  end
71
71
  end
72
72
 
@@ -35,21 +35,21 @@ describe "default values" do
35
35
  subject { Examples::Page.new }
36
36
 
37
37
  specify 'without a default the value is nil' do
38
- subject.title.should be_nil
38
+ expect(subject.title).to be_nil
39
39
  end
40
40
 
41
41
  specify 'can be supplied with the :default option' do
42
- subject.view_count.should == 0
42
+ expect(subject.view_count).to eq(0)
43
43
  end
44
44
 
45
45
  specify "you can pass a 'callable-object' to the :default option" do
46
46
  subject.title = 'Example Blog Post'
47
- subject.slug.should == 'example-blog-post'
47
+ expect(subject.slug).to eq('example-blog-post')
48
48
  end
49
49
 
50
50
  specify 'you can set defaults for private attributes' do
51
51
  subject.title = 'Top Secret'
52
- subject.editor_title.should == 'UNPUBLISHED: Top Secret'
52
+ expect(subject.editor_title).to eq('UNPUBLISHED: Top Secret')
53
53
  end
54
54
 
55
55
  specify 'you can reset attribute to its default' do
@@ -63,25 +63,25 @@ describe "default values" do
63
63
  it 'does not duplicate the ValueObject' do
64
64
  page1 = Examples::Page.new
65
65
  page2 = Examples::Page.new
66
- page1.reference.should equal(page2.reference)
66
+ expect(page1.reference).to equal(page2.reference)
67
67
  end
68
68
  end
69
69
 
70
70
  context 'an Array' do
71
71
  specify 'without a default the value is an empty Array' do
72
- subject.revisions.should eql([])
72
+ expect(subject.revisions).to eql([])
73
73
  end
74
74
  end
75
75
 
76
76
  context 'a Hash' do
77
77
  specify 'without a default the value is an empty Hash' do
78
- subject.index.should eql({})
78
+ expect(subject.index).to eql({})
79
79
  end
80
80
  end
81
81
 
82
82
  context 'a Set' do
83
83
  specify 'without a default the value is an empty Set' do
84
- subject.authors.should eql(Set.new)
84
+ expect(subject.authors).to eql(Set.new)
85
85
  end
86
86
  end
87
87
  end
@@ -19,61 +19,68 @@ describe "virtus attribute definitions" do
19
19
  end
20
20
  end
21
21
 
22
- subject { Examples::Person.new }
22
+ subject(:person) { Examples::Person.new(attributes) }
23
+
24
+ let(:attributes) { {} }
23
25
 
24
26
  specify 'virtus creates accessor methods' do
25
- subject.name = 'Peter'
26
- subject.name.should == 'Peter'
27
+ person.name = 'Peter'
28
+ expect(person.name).to eq('Peter')
27
29
  end
28
30
 
29
31
  specify 'the constructor accepts a hash for mass-assignment' do
30
32
  john = Examples::Person.new(:name => 'John', :age => 13)
31
- john.name.should == 'John'
32
- john.age.should == 13
33
+ expect(john.name).to eq('John')
34
+ expect(john.age).to eq(13)
33
35
  end
34
36
 
35
- specify 'Boolean attributes have a getter with questionmark' do
36
- subject.doctor?.should be_false
37
- subject.doctor = true
38
- subject.doctor?.should be_true
37
+ specify 'Boolean attributes have a predicate method' do
38
+ expect(person).not_to be_doctor
39
+ person.doctor = true
40
+ expect(person).to be_doctor
39
41
  end
40
42
 
41
43
  context 'with attributes' do
42
44
  let(:attributes) { {:name => 'Jane', :age => 45, :doctor => true, :salary => 4500} }
43
- subject { Examples::Person.new(attributes) }
44
45
 
45
46
  specify "#attributes returns the object's attributes as a hash" do
46
- subject.attributes.should == attributes
47
+ expect(person.attributes).to eq(attributes)
47
48
  end
48
49
 
49
50
  specify "#to_hash returns the object's attributes as a hash" do
50
- subject.to_hash.should == attributes
51
+ expect(person.to_hash).to eq(attributes)
52
+ end
53
+
54
+ specify "#to_h returns the object's attributes as a hash" do
55
+ expect(person.to_h).to eql(attributes)
51
56
  end
52
57
  end
53
58
 
54
59
  context 'inheritance' do
55
60
  specify 'inherits all the attributes from the base class' do
56
61
  fred = Examples::Manager.new(:name => 'Fred', :age => 29)
57
- fred.name.should == 'Fred'
58
- fred.age.should == 29
62
+ expect(fred.name).to eq('Fred')
63
+ expect(fred.age).to eq(29)
59
64
  end
60
65
 
61
66
  specify 'lets you add attributes to the base class at runtime' do
62
67
  frank = Examples::Manager.new(:name => 'Frank')
63
68
  Examples::Person.attribute :just_added, String
64
69
  frank.just_added = 'it works!'
65
- frank.just_added.should == 'it works!'
70
+ expect(frank.just_added).to eq('it works!')
66
71
  end
67
72
 
68
73
  specify 'lets you add attributes to the subclass at runtime' do
69
74
  person_jack = Examples::Person.new(:name => 'Jack')
70
75
  manager_frank = Examples::Manager.new(:name => 'Frank')
76
+
71
77
  Examples::Manager.attribute :just_added, String
72
78
 
73
79
  manager_frank.just_added = 'awesome!'
74
- manager_frank.just_added.should == 'awesome!'
75
- person_jack.should_not respond_to(:just_added)
76
- person_jack.should_not respond_to(:just_added=)
80
+
81
+ expect(manager_frank.just_added).to eq('awesome!')
82
+ expect(person_jack).not_to respond_to(:just_added)
83
+ expect(person_jack).not_to respond_to(:just_added=)
77
84
  end
78
85
  end
79
86
  end
@@ -33,18 +33,18 @@ describe 'embedded values' do
33
33
  end
34
34
 
35
35
  specify '#attributes returns instances of the embedded values' do
36
- subject.attributes.should == {
36
+ expect(subject.attributes).to eq({
37
37
  :name => 'the guy',
38
38
  :address => subject.address
39
- }
39
+ })
40
40
  end
41
41
 
42
42
  specify 'allows you to pass a hash for the embedded value' do
43
43
  user = Examples::User.new
44
44
  user.address = address_attributes
45
- user.address.street.should == 'Street 1/2'
46
- user.address.zipcode.should == '12345'
47
- user.address.city.name.should == 'NYC'
45
+ expect(user.address.street).to eq('Street 1/2')
46
+ expect(user.address.zipcode).to eq('12345')
47
+ expect(user.address.city.name).to eq('NYC')
48
48
  end
49
49
 
50
50
  end
@@ -21,15 +21,15 @@ describe 'I can extend objects' do
21
21
  admin.name = 'John'
22
22
  admin.age = 29
23
23
 
24
- admin.name.should eql('John')
25
- admin.age.should eql(29)
24
+ expect(admin.name).to eql('John')
25
+ expect(admin.age).to eql(29)
26
26
 
27
- admin.attributes.should eql(attributes)
27
+ expect(admin.attributes).to eql(attributes)
28
28
 
29
29
  new_attributes = { :name => 'Jane', :age => 28 }
30
30
  admin.attributes = new_attributes
31
31
 
32
- admin.name.should eql('Jane')
33
- admin.age.should eql(28)
32
+ expect(admin.name).to eql('Jane')
33
+ expect(admin.age).to eql(28)
34
34
  end
35
35
  end