virtus 1.0.1 → 2.0.0

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