virtus 0.2.0 → 0.3.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 (54) hide show
  1. data/.pelusa.yml +7 -0
  2. data/.travis.yml +5 -3
  3. data/Changelog.md +17 -0
  4. data/Gemfile +4 -0
  5. data/README.md +35 -39
  6. data/TODO +12 -7
  7. data/config/flay.yml +1 -1
  8. data/config/flog.yml +1 -1
  9. data/lib/virtus.rb +8 -0
  10. data/lib/virtus/attribute.rb +8 -29
  11. data/lib/virtus/attribute/boolean.rb +1 -1
  12. data/lib/virtus/attribute/default_value.rb +15 -45
  13. data/lib/virtus/attribute/default_value/from_callable.rb +33 -0
  14. data/lib/virtus/attribute/default_value/from_clonable.rb +40 -0
  15. data/lib/virtus/attribute/default_value/from_symbol.rb +35 -0
  16. data/lib/virtus/attribute/embedded_value.rb +3 -14
  17. data/lib/virtus/class_methods.rb +17 -0
  18. data/lib/virtus/coercion/hash.rb +0 -11
  19. data/lib/virtus/coercion/object.rb +98 -0
  20. data/lib/virtus/coercion/string.rb +9 -2
  21. data/lib/virtus/coercion/time_coercions.rb +2 -5
  22. data/lib/virtus/instance_methods.rb +16 -37
  23. data/lib/virtus/support/type_lookup.rb +1 -2
  24. data/lib/virtus/value_object.rb +31 -8
  25. data/lib/virtus/value_object/equalizer.rb +21 -26
  26. data/lib/virtus/version.rb +1 -1
  27. data/spec/integration/custom_attributes_spec.rb +1 -1
  28. data/spec/integration/default_values_spec.rb +15 -3
  29. data/spec/integration/defining_attributes_spec.rb +1 -1
  30. data/spec/integration/mass_assignment_with_accessors_spec.rb +44 -0
  31. data/spec/integration/virtus/value_object_spec.rb +2 -2
  32. data/spec/spec_helper.rb +8 -1
  33. data/spec/unit/virtus/attribute/class_methods/determine_type_spec.rb +1 -1
  34. data/spec/unit/virtus/attribute/default_spec.rb +1 -1
  35. data/spec/unit/virtus/attribute/default_value/evaluate_spec.rb +25 -11
  36. data/spec/unit/virtus/attribute/embedded_value/class_methods/merge_options_spec.rb +1 -1
  37. data/spec/unit/virtus/attribute/embedded_value/coerce_spec.rb +1 -1
  38. data/spec/unit/virtus/class_methods/allowed_writer_methods_spec.rb +25 -0
  39. data/spec/unit/virtus/coercion/object/class_methods/to_array_spec.rb +51 -0
  40. data/spec/unit/virtus/coercion/object/class_methods/to_hash_spec.rb +22 -0
  41. data/spec/unit/virtus/coercion/object/class_methods/to_integer_spec.rb +22 -0
  42. data/spec/unit/virtus/coercion/object/class_methods/to_string_spec.rb +22 -0
  43. data/spec/unit/virtus/coercion/string/class_methods/to_constant_spec.rb +37 -1
  44. data/spec/unit/virtus/instance_methods/attributes_spec.rb +14 -2
  45. data/spec/unit/virtus/value_object/class_methods/allowed_writer_methods_spec.rb +15 -0
  46. data/spec/unit/virtus/value_object/class_methods/equalizer_spec.rb +1 -1
  47. data/spec/unit/virtus/value_object/initialize_spec.rb +1 -1
  48. data/spec/unit/virtus/value_object/with_spec.rb +1 -1
  49. metadata +17 -10
  50. data/spec/unit/virtus/attribute/default_value/instance_methods/evaluate_spec.rb +0 -30
  51. data/spec/unit/virtus/attribute/instance_variable_name_spec.rb +0 -12
  52. data/spec/unit/virtus/attribute/reader_visibility_spec.rb +0 -24
  53. data/spec/unit/virtus/attribute/writer_visibility_spec.rb +0 -24
  54. data/spec/unit/virtus/coercion/hash/class_methods/to_array_spec.rb +0 -12
@@ -1,20 +1,8 @@
1
1
  module Virtus
2
2
  module ValueObject
3
+
3
4
  # A type of Module for dynamically defining and hosting equality methods
4
5
  class Equalizer < Module
5
- # Name of hosting Class or Module that will be used for #inspect
6
- #
7
- # @return [String]
8
- #
9
- # @api private
10
- attr_reader :host_name
11
-
12
- # List of methods that will be used to define equality methods
13
- #
14
- # @return [Array(Symbol)]
15
- #
16
- # @api private
17
- attr_reader :keys
18
6
 
19
7
  # Initialize an Equalizer with the given keys
20
8
  #
@@ -23,8 +11,7 @@ module Virtus
23
11
  #
24
12
  # @api private
25
13
  def initialize(host_name, keys = [])
26
- @host_name = host_name
27
- @keys = keys
14
+ @host_name, @keys = host_name, keys
28
15
  end
29
16
 
30
17
  # Append a key and compile the equality methods
@@ -35,8 +22,6 @@ module Virtus
35
22
  def <<(key)
36
23
  @keys << key
37
24
  compile
38
-
39
- self
40
25
  end
41
26
 
42
27
  # Compile the equalizer methods based on #keys
@@ -49,7 +34,6 @@ module Virtus
49
34
  define_eql_method
50
35
  define_equivalent_method
51
36
  define_hash_method
52
-
53
37
  self
54
38
  end
55
39
 
@@ -57,20 +41,20 @@ module Virtus
57
41
 
58
42
  # Define an inspect method that reports the values of the instance's keys
59
43
  #
60
- # @return [self]
44
+ # @return [undefined]
61
45
  #
62
46
  # @api private
63
47
  def define_inspect_method
64
48
  module_eval <<-RUBY, __FILE__, __LINE__ + 1
65
49
  def inspect
66
- "#<#{host_name} #{keys.map { |key| "#{key}=\#{#{key}.inspect}" }.join(' ')}>"
50
+ "#<#{@host_name} #{compile_keys { |key| "#{key}=\#{#{key}.inspect}" }}>"
67
51
  end
68
52
  RUBY
69
53
  end
70
54
 
71
55
  # Define an #eql? method based on the instance's values identified by #keys
72
56
  #
73
- # @return [self]
57
+ # @return [undefined]
74
58
  #
75
59
  # @api private
76
60
  def define_eql_method
@@ -78,14 +62,14 @@ module Virtus
78
62
  def eql?(other)
79
63
  return true if equal?(other)
80
64
  instance_of?(other.class) &&
81
- #{keys.map { |key| "#{key}.eql?(other.#{key})" }.join(' && ')}
65
+ #{compile_keys(' && ') { |key| "#{key}.eql?(other.#{key})" }}
82
66
  end
83
67
  RUBY
84
68
  end
85
69
 
86
70
  # Define an #== method based on the instance's values identified by #keys
87
71
  #
88
- # @return [self]
72
+ # @return [undefined]
89
73
  #
90
74
  # @api private
91
75
  def define_equivalent_method
@@ -101,29 +85,40 @@ module Virtus
101
85
 
102
86
  # Define a #hash method based on the instance's values identified by #keys
103
87
  #
104
- # @return [self]
88
+ # @return [undefined]
105
89
  #
106
90
  # @api private
107
91
  def define_hash_method
108
92
  module_eval <<-RUBY, __FILE__, __LINE__ + 1
109
93
  def hash
110
- self.class.hash ^ #{keys.map { |key| "#{key}.hash" }.join(' ^ ')}
94
+ self.class.hash ^ #{compile_keys(' ^ ') { |key| "#{key}.hash" }}
111
95
  end
112
96
  RUBY
113
97
  end
114
98
 
99
+ # Return a list of strings containing ruby code for method generation
100
+ #
101
+ # @return [Array(Array<String>, Array<String>)]
102
+ #
115
103
  # @api private
116
104
  def compile_strings_for_equivalent_method
117
105
  respond_to = []
118
106
  equivalent = []
119
107
 
120
- keys.each do |key|
108
+ @keys.each do |key|
121
109
  respond_to << "other.respond_to?(#{key.inspect})"
122
110
  equivalent << "#{key} == other.#{key}"
123
111
  end
124
112
 
125
113
  [ respond_to, equivalent ]
126
114
  end
115
+
116
+ # @api private
117
+ def compile_keys(separator = ' ', &block)
118
+ keys_map = @keys.map { |key| yield(key) }
119
+ keys_map.join(separator)
120
+ end
121
+
127
122
  end # class Equalizer
128
123
  end # module ValueObject
129
124
  end # module Virtus
@@ -1,3 +1,3 @@
1
1
  module Virtus
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe 'custom attribtues' do
3
+ describe 'custom attributes' do
4
4
 
5
5
  module Virtus
6
6
  class Coercion
@@ -7,10 +7,17 @@ describe "default values" do
7
7
  class Page
8
8
  include Virtus
9
9
 
10
- attribute :title, String
11
- attribute :slug, String, :default => lambda { |post, attribute| post.title.downcase.gsub(' ', '-') }
12
- attribute :view_count, Integer, :default => 0
10
+ attribute :title, String
11
+ attribute :slug, String, :default => lambda { |post, attribute| post.title.downcase.gsub(' ', '-') }
12
+ attribute :view_count, Integer, :default => 0
13
+ attribute :published, Boolean, :default => false, :accessor => :private
14
+ attribute :editor_title, String, :default => :default_editor_title
15
+
16
+ def default_editor_title
17
+ published? ? title : "UNPUBLISHED: #{title}"
18
+ end
13
19
  end
20
+
14
21
  end
15
22
  end
16
23
 
@@ -29,4 +36,9 @@ describe "default values" do
29
36
  subject.slug.should == 'example-blog-post'
30
37
  end
31
38
 
39
+ specify 'you can set defaults for private attributes' do
40
+ subject.title = 'Top Secret'
41
+ subject.editor_title.should == 'UNPUBLISHED: Top Secret'
42
+ end
43
+
32
44
  end
@@ -52,7 +52,7 @@ describe "virtus attribute definitions" do
52
52
  end
53
53
 
54
54
  context 'inheritance' do
55
- specify 'inherits all the attribtues from the base class' do
55
+ specify 'inherits all the attributes from the base class' do
56
56
  fred = Examples::Manager.new(:name => 'Fred', :age => 29)
57
57
  fred.name.should == 'Fred'
58
58
  fred.age.should == 29
@@ -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
+ subject.category.should == 'Office'
32
+ subject.subcategory.should == 'Printers'
33
+ end
34
+
35
+ specify 'can be set with #attributes=' do
36
+ subject.attributes = {:categories => ['Home', 'Furniture']}
37
+ subject.category.should == 'Home'
38
+ subject.subcategory.should == 'Furniture'
39
+ end
40
+
41
+ specify 'respects accessor visibility' do
42
+ subject.id.should_not == 100
43
+ end
44
+ end
@@ -29,8 +29,8 @@ describe Virtus::ValueObject do
29
29
 
30
30
  describe 'writer visibility' do
31
31
  it 'attributes are configured for private writers' do
32
- class_under_test.attributes[:latitude].writer_visibility.should == :private
33
- class_under_test.attributes[:longitude].writer_visibility.should == :private
32
+ class_under_test.attributes[:latitude].public_reader?.should be(true)
33
+ class_under_test.attributes[:longitude].public_writer?.should be(false)
34
34
  end
35
35
 
36
36
  it 'writer methods are set to private' do
@@ -1,5 +1,5 @@
1
- require 'backports'
2
1
  require 'rubygems'
2
+ require 'backports'
3
3
 
4
4
  begin
5
5
  require 'rspec' # try for RSpec 2
@@ -38,3 +38,10 @@ RSpec.configure do |config|
38
38
  end
39
39
 
40
40
  end
41
+
42
+ # change the heckle timeout to be 5 seconds
43
+ if defined?(::Heckle)
44
+ class ::Heckle
45
+ @@timeout = 5
46
+ end
47
+ end
@@ -17,7 +17,7 @@ describe Virtus::Attribute, '.determine_type' do
17
17
  subject { object.determine_type(primitive) }
18
18
 
19
19
  before do
20
- if [Virtus::Attribute::EmbeddedValue, Virtus::Attribute::Collection].include? attribute_class
20
+ if [Virtus::Attribute::Collection].include? attribute_class
21
21
  pending
22
22
  end
23
23
  end
@@ -23,7 +23,7 @@ describe Virtus::Attribute, '#default' do
23
23
  options.update(:default => default)
24
24
  end
25
25
 
26
- it { should be_instance_of(Virtus::Attribute::DefaultValue) }
26
+ it { should be_instance_of(Virtus::Attribute::DefaultValue::FromClonable) }
27
27
 
28
28
  its(:attribute) { should be(object) }
29
29
 
@@ -3,17 +3,16 @@ require 'spec_helper'
3
3
  describe Virtus::Attribute::DefaultValue, '#evaluate' do
4
4
  subject { object.evaluate(instance) }
5
5
 
6
- let(:object) { described_class.new(attribute, value) }
7
- let(:attribute) { mock('attribute') }
8
- let(:value) { mock('value') }
9
- let(:instance) { mock('instance') }
6
+ let(:object) { described_class.build(attribute, value) }
7
+ let(:attribute) { mock('attribute') }
8
+ let(:value) { mock('value') }
9
+ let(:instance) { mock('instance') }
10
+ let(:response) { stub('response') }
10
11
 
11
12
  context 'when the value is callable' do
12
- let(:response) { stub('response') }
13
+ before { value.stub(:call => response) }
13
14
 
14
- before do
15
- value.stub(:call => response)
16
- end
15
+ specify { object.should be_instance_of(Virtus::Attribute::DefaultValue::FromCallable) }
17
16
 
18
17
  it { should be(response) }
19
18
 
@@ -26,9 +25,9 @@ describe Virtus::Attribute::DefaultValue, '#evaluate' do
26
25
  context 'when the value is cloneable' do
27
26
  let(:clone) { stub('clone') }
28
27
 
29
- before do
30
- value.stub(:clone => clone)
31
- end
28
+ before { value.stub(:clone => clone) }
29
+
30
+ specify { object.should be_instance_of(Virtus::Attribute::DefaultValue::FromClonable) }
32
31
 
33
32
  it { should be(clone) }
34
33
 
@@ -38,6 +37,21 @@ describe Virtus::Attribute::DefaultValue, '#evaluate' do
38
37
  end
39
38
  end
40
39
 
40
+ context 'when the value is a method name' do
41
+ let(:instance) { mock('instance', value => retval) }
42
+ let(:value) { :set_default }
43
+ let(:retval) { stub('retval') }
44
+
45
+ specify { object.should be_instance_of(Virtus::Attribute::DefaultValue::FromSymbol) }
46
+
47
+ it { should be(retval) }
48
+
49
+ it 'calls the method' do
50
+ instance.should_receive(value).with(no_args)
51
+ subject
52
+ end
53
+ end
54
+
41
55
  # smallest number that is Bignum across major ruby impls
42
56
  bignum = 0x7fffffffffffffff + 1
43
57
 
@@ -12,6 +12,6 @@ describe Virtus::Attribute::EmbeddedValue, '.merge_options' do
12
12
  it { should_not equal(options) }
13
13
 
14
14
  it 'merges the type in as the model' do
15
- should eql(:model => type)
15
+ should eql(:primitive => type)
16
16
  end
17
17
  end
@@ -11,7 +11,7 @@ describe Virtus::Attribute::EmbeddedValue, '#coerce' do
11
11
  let(:value) { Hash[:foo => 'bar'] }
12
12
 
13
13
  context 'when the options include the model' do
14
- let(:object) { described_class.new(attribute_name, :model => model) }
14
+ let(:object) { described_class.new(attribute_name, :primitive => model) }
15
15
  let(:model) { mock('model') }
16
16
  let(:model_instance) { mock('model_instance') }
17
17
 
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus::ClassMethods, '#allowed_writer_methods' do
4
+ subject { object.allowed_writer_methods }
5
+
6
+ let(:object) do
7
+ Class.new do
8
+ include Virtus
9
+ attribute :virtus_attribute, String
10
+ attr_accessor :some_other_attribute
11
+ private
12
+ attr_accessor :private_attribute
13
+ end
14
+ end
15
+
16
+ it { should include('virtus_attribute=') }
17
+ it { should include('some_other_attribute=') }
18
+ it { should_not include('private_attribute=') }
19
+
20
+ %w[ == != === []= attributes= ].each do |invalid_method|
21
+ it { should_not include(invalid_method) }
22
+ end
23
+
24
+ it { should be_frozen }
25
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus::Coercion::Object, '.to_array' do
4
+ subject { object.to_array(value) }
5
+
6
+ let(:object) { described_class }
7
+ let(:value) { Object.new }
8
+ let(:coerced) { [ value ] }
9
+
10
+ context 'when the value responds to #to_ary' do
11
+ before do
12
+ value.should_receive(:to_ary).with().and_return(coerced)
13
+ end
14
+
15
+ it { should be(coerced) }
16
+
17
+ it 'does not call #to_a if #to_ary is available' do
18
+ value.should_not_receive(:to_a)
19
+ subject
20
+ end
21
+ end
22
+
23
+ context 'when the value responds to #to_a but not #to_ary' do
24
+ before do
25
+ value.should_receive(:to_a).with().and_return(coerced)
26
+ end
27
+
28
+ it { should be(coerced) }
29
+ end
30
+
31
+ context 'when the value does not respond to #to_ary or #to_a' do
32
+ it { should be_instance_of(Array) }
33
+
34
+ it { should == coerced }
35
+ end
36
+
37
+ context 'when the value returns nil from #to_ary' do
38
+ before do
39
+ value.should_receive(:to_ary).with().and_return(nil)
40
+ end
41
+
42
+ it 'calls #to_a as a fallback' do
43
+ value.should_receive(:to_a).with().and_return(coerced)
44
+ should be(coerced)
45
+ end
46
+
47
+ it 'wraps the value in an Array if #to_a is not available' do
48
+ should == coerced
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus::Coercion::Object, '.to_hash' do
4
+ subject { object.to_hash(value) }
5
+
6
+ let(:object) { described_class }
7
+ let(:value) { stub('value') }
8
+
9
+ context 'when the value responds to #to_hash' do
10
+ let(:coerced) { stub('coerced') }
11
+
12
+ before do
13
+ value.should_receive(:to_hash).with().and_return(coerced)
14
+ end
15
+
16
+ it { should be(coerced) }
17
+ end
18
+
19
+ context 'when the value does not respond to #to_hash' do
20
+ it { should be(value) }
21
+ end
22
+ end