virtus2 2.0.1

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