virtus2 2.0.1

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 (118) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +39 -0
  3. data/.rspec +2 -0
  4. data/.yardopts +1 -0
  5. data/CONTRIBUTING.md +18 -0
  6. data/Changelog.md +258 -0
  7. data/Gemfile +10 -0
  8. data/Guardfile +19 -0
  9. data/LICENSE +20 -0
  10. data/README.md +630 -0
  11. data/Rakefile +15 -0
  12. data/TODO.md +6 -0
  13. data/lib/virtus/attribute/accessor.rb +103 -0
  14. data/lib/virtus/attribute/boolean.rb +55 -0
  15. data/lib/virtus/attribute/builder.rb +182 -0
  16. data/lib/virtus/attribute/coercer.rb +45 -0
  17. data/lib/virtus/attribute/coercible.rb +20 -0
  18. data/lib/virtus/attribute/collection.rb +103 -0
  19. data/lib/virtus/attribute/default_value/from_callable.rb +35 -0
  20. data/lib/virtus/attribute/default_value/from_clonable.rb +35 -0
  21. data/lib/virtus/attribute/default_value/from_symbol.rb +35 -0
  22. data/lib/virtus/attribute/default_value.rb +51 -0
  23. data/lib/virtus/attribute/embedded_value.rb +67 -0
  24. data/lib/virtus/attribute/enum.rb +45 -0
  25. data/lib/virtus/attribute/hash.rb +130 -0
  26. data/lib/virtus/attribute/lazy_default.rb +18 -0
  27. data/lib/virtus/attribute/nullify_blank.rb +24 -0
  28. data/lib/virtus/attribute/strict.rb +26 -0
  29. data/lib/virtus/attribute.rb +245 -0
  30. data/lib/virtus/attribute_set.rb +240 -0
  31. data/lib/virtus/builder/hook_context.rb +51 -0
  32. data/lib/virtus/builder.rb +133 -0
  33. data/lib/virtus/class_inclusions.rb +48 -0
  34. data/lib/virtus/class_methods.rb +90 -0
  35. data/lib/virtus/coercer.rb +41 -0
  36. data/lib/virtus/configuration.rb +72 -0
  37. data/lib/virtus/const_missing_extensions.rb +18 -0
  38. data/lib/virtus/extensions.rb +105 -0
  39. data/lib/virtus/instance_methods.rb +218 -0
  40. data/lib/virtus/model.rb +68 -0
  41. data/lib/virtus/module_extensions.rb +88 -0
  42. data/lib/virtus/support/equalizer.rb +128 -0
  43. data/lib/virtus/support/options.rb +113 -0
  44. data/lib/virtus/support/type_lookup.rb +109 -0
  45. data/lib/virtus/value_object.rb +150 -0
  46. data/lib/virtus/version.rb +3 -0
  47. data/lib/virtus.rb +310 -0
  48. data/spec/integration/attributes_attribute_spec.rb +28 -0
  49. data/spec/integration/building_module_spec.rb +90 -0
  50. data/spec/integration/collection_member_coercion_spec.rb +96 -0
  51. data/spec/integration/custom_attributes_spec.rb +42 -0
  52. data/spec/integration/custom_collection_attributes_spec.rb +101 -0
  53. data/spec/integration/default_values_spec.rb +87 -0
  54. data/spec/integration/defining_attributes_spec.rb +86 -0
  55. data/spec/integration/embedded_value_spec.rb +50 -0
  56. data/spec/integration/extending_objects_spec.rb +35 -0
  57. data/spec/integration/hash_attributes_coercion_spec.rb +54 -0
  58. data/spec/integration/inheritance_spec.rb +42 -0
  59. data/spec/integration/injectible_coercers_spec.rb +48 -0
  60. data/spec/integration/mass_assignment_with_accessors_spec.rb +44 -0
  61. data/spec/integration/overriding_virtus_spec.rb +46 -0
  62. data/spec/integration/required_attributes_spec.rb +25 -0
  63. data/spec/integration/struct_as_embedded_value_spec.rb +28 -0
  64. data/spec/integration/using_modules_spec.rb +55 -0
  65. data/spec/integration/value_object_with_custom_constructor_spec.rb +42 -0
  66. data/spec/integration/virtus/instance_level_attributes_spec.rb +23 -0
  67. data/spec/integration/virtus/value_object_spec.rb +99 -0
  68. data/spec/shared/constants_helpers.rb +9 -0
  69. data/spec/shared/freeze_method_behavior.rb +40 -0
  70. data/spec/shared/idempotent_method_behaviour.rb +5 -0
  71. data/spec/shared/options_class_method.rb +19 -0
  72. data/spec/spec_helper.rb +41 -0
  73. data/spec/unit/virtus/attribute/boolean/coerce_spec.rb +43 -0
  74. data/spec/unit/virtus/attribute/boolean/value_coerced_predicate_spec.rb +25 -0
  75. data/spec/unit/virtus/attribute/class_methods/build_spec.rb +180 -0
  76. data/spec/unit/virtus/attribute/class_methods/coerce_spec.rb +32 -0
  77. data/spec/unit/virtus/attribute/coerce_spec.rb +129 -0
  78. data/spec/unit/virtus/attribute/coercible_predicate_spec.rb +20 -0
  79. data/spec/unit/virtus/attribute/collection/class_methods/build_spec.rb +105 -0
  80. data/spec/unit/virtus/attribute/collection/coerce_spec.rb +74 -0
  81. data/spec/unit/virtus/attribute/collection/value_coerced_predicate_spec.rb +31 -0
  82. data/spec/unit/virtus/attribute/comparison_spec.rb +20 -0
  83. data/spec/unit/virtus/attribute/custom_collection_spec.rb +29 -0
  84. data/spec/unit/virtus/attribute/defined_spec.rb +20 -0
  85. data/spec/unit/virtus/attribute/embedded_value/class_methods/build_spec.rb +70 -0
  86. data/spec/unit/virtus/attribute/embedded_value/coerce_spec.rb +91 -0
  87. data/spec/unit/virtus/attribute/get_spec.rb +32 -0
  88. data/spec/unit/virtus/attribute/hash/class_methods/build_spec.rb +106 -0
  89. data/spec/unit/virtus/attribute/hash/coerce_spec.rb +92 -0
  90. data/spec/unit/virtus/attribute/lazy_predicate_spec.rb +20 -0
  91. data/spec/unit/virtus/attribute/rename_spec.rb +16 -0
  92. data/spec/unit/virtus/attribute/required_predicate_spec.rb +19 -0
  93. data/spec/unit/virtus/attribute/set_default_value_spec.rb +107 -0
  94. data/spec/unit/virtus/attribute/set_spec.rb +29 -0
  95. data/spec/unit/virtus/attribute/value_coerced_predicate_spec.rb +19 -0
  96. data/spec/unit/virtus/attribute_set/append_spec.rb +47 -0
  97. data/spec/unit/virtus/attribute_set/define_reader_method_spec.rb +36 -0
  98. data/spec/unit/virtus/attribute_set/define_writer_method_spec.rb +36 -0
  99. data/spec/unit/virtus/attribute_set/each_spec.rb +65 -0
  100. data/spec/unit/virtus/attribute_set/element_reference_spec.rb +17 -0
  101. data/spec/unit/virtus/attribute_set/element_set_spec.rb +64 -0
  102. data/spec/unit/virtus/attribute_set/merge_spec.rb +34 -0
  103. data/spec/unit/virtus/attribute_set/reset_spec.rb +71 -0
  104. data/spec/unit/virtus/attribute_spec.rb +229 -0
  105. data/spec/unit/virtus/attributes_reader_spec.rb +41 -0
  106. data/spec/unit/virtus/attributes_writer_spec.rb +51 -0
  107. data/spec/unit/virtus/class_methods/finalize_spec.rb +67 -0
  108. data/spec/unit/virtus/class_methods/new_spec.rb +39 -0
  109. data/spec/unit/virtus/config_spec.rb +13 -0
  110. data/spec/unit/virtus/element_reader_spec.rb +21 -0
  111. data/spec/unit/virtus/element_writer_spec.rb +19 -0
  112. data/spec/unit/virtus/freeze_spec.rb +41 -0
  113. data/spec/unit/virtus/model_spec.rb +197 -0
  114. data/spec/unit/virtus/module_spec.rb +174 -0
  115. data/spec/unit/virtus/set_default_attributes_spec.rb +32 -0
  116. data/spec/unit/virtus/value_object_spec.rb +138 -0
  117. data/virtus2.gemspec +26 -0
  118. metadata +225 -0
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Inheritance' do
4
+ before do
5
+ module Examples
6
+ class Base
7
+ include Virtus.model
8
+ end
9
+
10
+ class First < Base
11
+ attribute :id, Fixnum
12
+ attribute :name, String, default: ->(first, _) { "Named: #{first.id}" }
13
+ attribute :description, String
14
+ end
15
+
16
+ class Second < Base
17
+ attribute :something, String
18
+ end
19
+ end
20
+ end
21
+
22
+ it 'inherits model from the base class' do
23
+ expect(Examples::First.attribute_set.map(&:name)).to eql([:id, :name, :description])
24
+ expect(Examples::Second.attribute_set.map(&:name)).to eql([:something])
25
+ end
26
+
27
+ it 'sets correct attributes on the descendant classes' do
28
+ first = Examples::First.new(:id => 1, :description => 'hello world')
29
+
30
+ expect(first.id).to be(1)
31
+ expect(first.name).to eql('Named: 1')
32
+ expect(first.description).to eql('hello world')
33
+
34
+ second = Examples::Second.new
35
+
36
+ expect(second.something).to be(nil)
37
+
38
+ second.something = 'foo bar'
39
+
40
+ expect(second.something).to eql('foo bar')
41
+ end
42
+ end
@@ -0,0 +1,48 @@
1
+ require 'virtus'
2
+
3
+ describe 'Injectible coercer' do
4
+ before do
5
+ module Examples
6
+ class EmailAddress
7
+ include Virtus.value_object
8
+
9
+ values do
10
+ attribute :address, String, :coercer => lambda { |add| add.downcase }
11
+ end
12
+
13
+ def self.coerce(input)
14
+ if input.is_a?(String)
15
+ new(:address => input)
16
+ else
17
+ new(input)
18
+ end
19
+ end
20
+ end
21
+
22
+ class User
23
+ include Virtus.model
24
+
25
+ attribute :email, EmailAddress,
26
+ :coercer => lambda { |input| Examples::EmailAddress.coerce(input) }
27
+ end
28
+ end
29
+ end
30
+
31
+ after do
32
+ Examples.send(:remove_const, :EmailAddress)
33
+ Examples.send(:remove_const, :User)
34
+ end
35
+
36
+ let(:doe) { Examples::EmailAddress.new(:address => 'john.doe@example.com') }
37
+
38
+ it 'accepts an email hash' do
39
+ user = Examples::User.new :email => { :address => 'John.Doe@Example.Com' }
40
+ expect(user.email).to eq(doe)
41
+ end
42
+
43
+ it 'coerces an embedded string' do
44
+ user = Examples::User.new :email => 'John.Doe@Example.Com'
45
+ expect(user.email).to eq(doe)
46
+ end
47
+
48
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ describe "mass assignment with accessors" do
4
+
5
+ before do
6
+ module Examples
7
+ class Product
8
+ include Virtus
9
+
10
+ attribute :id, Integer
11
+ attribute :category, String
12
+ attribute :subcategory, String
13
+
14
+ def categories=(categories)
15
+ self.category = categories.first
16
+ self.subcategory = categories.last
17
+ end
18
+
19
+ private
20
+
21
+ def _id=(value)
22
+ self.id = value
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ subject { Examples::Product.new(:categories => ['Office', 'Printers'], :_id => 100) }
29
+
30
+ specify 'works uppon instantiation' do
31
+ expect(subject.category).to eq('Office')
32
+ expect(subject.subcategory).to eq('Printers')
33
+ end
34
+
35
+ specify 'can be set with #attributes=' do
36
+ subject.attributes = {:categories => ['Home', 'Furniture']}
37
+ expect(subject.category).to eq('Home')
38
+ expect(subject.subcategory).to eq('Furniture')
39
+ end
40
+
41
+ specify 'respects accessor visibility' do
42
+ expect(subject.id).not_to eq(100)
43
+ end
44
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'overriding virtus behavior' do
4
+
5
+ before do
6
+ module Examples
7
+ class Article
8
+ include Virtus
9
+
10
+ attribute :title, String
11
+
12
+ def title
13
+ super || '<unknown>'
14
+ end
15
+
16
+ def title=(name)
17
+ super unless self.title == "can't be changed"
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ describe 'overriding an attribute getter' do
24
+ specify 'calls the defined getter' do
25
+ expect(Examples::Article.new.title).to eq('<unknown>')
26
+ end
27
+
28
+ specify 'super can be used to access the getter defined by virtus' do
29
+ expect(Examples::Article.new(:title => 'example article').title).to eq('example article')
30
+ end
31
+ end
32
+
33
+ describe 'overriding an attribute setter' do
34
+ specify 'calls the defined setter' do
35
+ article = Examples::Article.new(:title => "can't be changed")
36
+ article.title = 'this will never be assigned'
37
+ expect(article.title).to eq("can't be changed")
38
+ end
39
+
40
+ specify 'super can be used to access the setter defined by virtus' do
41
+ article = Examples::Article.new(:title => 'example article')
42
+ article.title = 'my new title'
43
+ expect(article.title).to eq('my new title')
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Using required attributes' do
4
+ before do
5
+ module Examples
6
+ class User
7
+ include Virtus.model(:strict => true)
8
+
9
+ attribute :name, String
10
+ attribute :age, Integer, :required => false
11
+ end
12
+ end
13
+ end
14
+
15
+ it 'raises coercion error when required attribute is nil' do
16
+ expect { Examples::User.new(:name => nil) }.to raise_error(Virtus::CoercionError, "Failed to coerce attribute `name' from nil into String")
17
+ end
18
+
19
+ it 'does not raise coercion error when not required attribute is nil' do
20
+ user = Examples::User.new(:name => 'Jane', :age => nil)
21
+
22
+ expect(user.name).to eql('Jane')
23
+ expect(user.age).to be(nil)
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Using Struct as an embedded value attribute' do
4
+ before do
5
+ module Examples
6
+ Point = Struct.new(:x, :y)
7
+
8
+ class Rectangle
9
+ include Virtus
10
+
11
+ attribute :top_left, Point
12
+ attribute :bottom_right, Point
13
+ end
14
+ end
15
+ end
16
+
17
+ subject do
18
+ Examples::Rectangle.new(:top_left => [ 3, 5 ], :bottom_right => [ 8, 7 ])
19
+ end
20
+
21
+ specify 'initialize a struct object with correct attributes' do
22
+ expect(subject.top_left.x).to be(3)
23
+ expect(subject.top_left.y).to be(5)
24
+
25
+ expect(subject.bottom_right.x).to be(8)
26
+ expect(subject.bottom_right.y).to be(7)
27
+ end
28
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'I can define attributes within a module' do
4
+ before do
5
+ module Examples
6
+ module Common
7
+ include Virtus
8
+ end
9
+
10
+ module Name
11
+ include Common
12
+
13
+ attribute :name, String
14
+ attribute :gamer, Boolean
15
+ end
16
+
17
+ module Age
18
+ include Common
19
+
20
+ attribute :age, Integer
21
+ end
22
+
23
+ class User
24
+ include Name
25
+ end
26
+
27
+ class Admin < User
28
+ include Age
29
+ end
30
+
31
+ class Moderator; end
32
+ end
33
+ end
34
+
35
+ specify 'including a module with attributes into a class' do
36
+ expect(Examples::User.attribute_set[:name]).to be_instance_of(Virtus::Attribute)
37
+ expect(Examples::User.attribute_set[:gamer]).to be_instance_of(Virtus::Attribute::Boolean)
38
+
39
+ expect(Examples::Admin.attribute_set[:name]).to be_instance_of(Virtus::Attribute)
40
+ expect(Examples::Admin.attribute_set[:age]).to be_instance_of(Virtus::Attribute)
41
+
42
+ user = Examples::Admin.new(:name => 'Piotr', :age => 29)
43
+ expect(user.name).to eql('Piotr')
44
+ expect(user.age).to eql(29)
45
+ end
46
+
47
+ specify 'including a module with attributes into an instance' do
48
+ moderator = Examples::Moderator.new
49
+ moderator.extend(Examples::Name, Examples::Age)
50
+
51
+ moderator.attributes = { :name => 'John', :age => 21 }
52
+ expect(moderator.name).to eql('John')
53
+ expect(moderator.age).to eql(21)
54
+ end
55
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Defining a ValueObject with a custom constructor" do
4
+ before do
5
+ module Examples
6
+ class Point
7
+ include Virtus::ValueObject
8
+
9
+ attribute :x, Integer
10
+ attribute :y, Integer
11
+
12
+ def initialize(attributes)
13
+ if attributes.kind_of?(Array)
14
+ self.x = attributes.first
15
+ self.y = attributes.last
16
+ else
17
+ super
18
+ end
19
+ end
20
+ end
21
+
22
+ class Rectangle
23
+ include Virtus
24
+
25
+ attribute :top_left, Point
26
+ attribute :bottom_right, Point
27
+ end
28
+ end
29
+ end
30
+
31
+ subject do
32
+ Examples::Rectangle.new(:top_left => [ 3, 4 ], :bottom_right => [ 5, 8 ])
33
+ end
34
+
35
+ specify "initialize a value object attribute with correct attributes" do
36
+ expect(subject.top_left.x).to be(3)
37
+ expect(subject.top_left.y).to be(4)
38
+
39
+ expect(subject.bottom_right.x).to be(5)
40
+ expect(subject.bottom_right.y).to be(8)
41
+ end
42
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus, 'instance level attributes' do
4
+ subject do
5
+ subject = Object.new
6
+ subject.singleton_class.send(:include, Virtus)
7
+ subject
8
+ end
9
+
10
+ let(:attribute) { subject.singleton_class.attribute(:name, String) }
11
+
12
+ before do
13
+ pending if RUBY_VERSION < '1.9'
14
+ end
15
+
16
+ context 'adding an attribute' do
17
+ it 'allows setting the attribute value on the instance' do
18
+ attribute
19
+ subject.name = 'foo'
20
+ expect(subject.name).to eql('foo')
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,99 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus::ValueObject do
4
+ let(:class_under_test) do
5
+ Class.new do
6
+ def self.name
7
+ 'GeoLocation'
8
+ end
9
+
10
+ include Virtus::ValueObject
11
+
12
+ attribute :latitude, Float
13
+ attribute :longitude, Float
14
+ end
15
+ end
16
+
17
+ let(:attribute_values) { { :latitude => 10.0, :longitude => 20.0 } }
18
+
19
+ let(:instance_with_equal_state) { class_under_test.new(attribute_values) }
20
+
21
+ let(:instance_with_different_state) do
22
+ class_under_test.new(:latitude => attribute_values[:latitude])
23
+ end
24
+
25
+ subject { class_under_test.new(attribute_values) }
26
+
27
+ describe 'initialization' do
28
+ it 'sets the attribute values provided to Class.new' do
29
+ expect(class_under_test.new(:latitude => 10000.001).latitude).to eq(10000.001)
30
+ expect(subject.latitude).to eql(attribute_values[:latitude])
31
+ end
32
+ end
33
+
34
+ describe 'writer visibility' do
35
+ it 'attributes are configured for private writers' do
36
+ expect(class_under_test.attribute_set[:latitude].public_reader?).to be(true)
37
+ expect(class_under_test.attribute_set[:longitude].public_writer?).to be(false)
38
+ end
39
+
40
+ it 'writer methods are set to private' do
41
+ private_methods = class_under_test.private_instance_methods
42
+ private_methods.map! { |m| m.to_s }
43
+ expect(private_methods).to include('latitude=', 'longitude=', 'attributes=')
44
+ end
45
+
46
+ it 'attempts to call attribute writer methods raises NameError' do
47
+ expect { subject.latitude = 5.0 }.to raise_exception(NameError)
48
+ expect { subject.longitude = 5.0 }.to raise_exception(NameError)
49
+ end
50
+ end
51
+
52
+ describe 'equality' do
53
+ describe '#==' do
54
+ it 'returns true for different objects with the same state' do
55
+ expect(subject).to eq(instance_with_equal_state)
56
+ end
57
+
58
+ it 'returns false for different objects with different state' do
59
+ expect(subject).not_to eq(instance_with_different_state)
60
+ end
61
+ end
62
+
63
+ describe '#eql?' do
64
+ it 'returns true for different objects with the same state' do
65
+ expect(subject).to eql(instance_with_equal_state)
66
+ end
67
+
68
+ it 'returns false for different objects with different state' do
69
+ expect(subject).not_to eql(instance_with_different_state)
70
+ end
71
+ end
72
+
73
+ describe '#equal?' do
74
+ it 'returns false for different objects with the same state' do
75
+ expect(subject).not_to equal(instance_with_equal_state)
76
+ end
77
+
78
+ it 'returns false for different objects with different state' do
79
+ expect(subject).not_to equal(instance_with_different_state)
80
+ end
81
+ end
82
+
83
+ describe '#hash' do
84
+ it 'returns the same value for different objects with the same state' do
85
+ expect(subject.hash).to eql(instance_with_equal_state.hash)
86
+ end
87
+
88
+ it 'returns different values for different objects with different state' do
89
+ expect(subject.hash).not_to eql(instance_with_different_state.hash)
90
+ end
91
+ end
92
+ end
93
+
94
+ describe '#inspect' do
95
+ it 'includes the class name and attribute values' do
96
+ expect(subject.inspect).to eq('#<GeoLocation latitude=10.0 longitude=20.0>')
97
+ end
98
+ end
99
+ end