virtus 0.2.0 → 0.3.0

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