virtus 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.pelusa.yml +7 -0
- data/.travis.yml +5 -3
- data/Changelog.md +17 -0
- data/Gemfile +4 -0
- data/README.md +35 -39
- data/TODO +12 -7
- data/config/flay.yml +1 -1
- data/config/flog.yml +1 -1
- data/lib/virtus.rb +8 -0
- data/lib/virtus/attribute.rb +8 -29
- data/lib/virtus/attribute/boolean.rb +1 -1
- data/lib/virtus/attribute/default_value.rb +15 -45
- data/lib/virtus/attribute/default_value/from_callable.rb +33 -0
- data/lib/virtus/attribute/default_value/from_clonable.rb +40 -0
- data/lib/virtus/attribute/default_value/from_symbol.rb +35 -0
- data/lib/virtus/attribute/embedded_value.rb +3 -14
- data/lib/virtus/class_methods.rb +17 -0
- data/lib/virtus/coercion/hash.rb +0 -11
- data/lib/virtus/coercion/object.rb +98 -0
- data/lib/virtus/coercion/string.rb +9 -2
- data/lib/virtus/coercion/time_coercions.rb +2 -5
- data/lib/virtus/instance_methods.rb +16 -37
- data/lib/virtus/support/type_lookup.rb +1 -2
- data/lib/virtus/value_object.rb +31 -8
- data/lib/virtus/value_object/equalizer.rb +21 -26
- data/lib/virtus/version.rb +1 -1
- data/spec/integration/custom_attributes_spec.rb +1 -1
- data/spec/integration/default_values_spec.rb +15 -3
- data/spec/integration/defining_attributes_spec.rb +1 -1
- data/spec/integration/mass_assignment_with_accessors_spec.rb +44 -0
- data/spec/integration/virtus/value_object_spec.rb +2 -2
- data/spec/spec_helper.rb +8 -1
- data/spec/unit/virtus/attribute/class_methods/determine_type_spec.rb +1 -1
- data/spec/unit/virtus/attribute/default_spec.rb +1 -1
- data/spec/unit/virtus/attribute/default_value/evaluate_spec.rb +25 -11
- data/spec/unit/virtus/attribute/embedded_value/class_methods/merge_options_spec.rb +1 -1
- data/spec/unit/virtus/attribute/embedded_value/coerce_spec.rb +1 -1
- data/spec/unit/virtus/class_methods/allowed_writer_methods_spec.rb +25 -0
- data/spec/unit/virtus/coercion/object/class_methods/to_array_spec.rb +51 -0
- data/spec/unit/virtus/coercion/object/class_methods/to_hash_spec.rb +22 -0
- data/spec/unit/virtus/coercion/object/class_methods/to_integer_spec.rb +22 -0
- data/spec/unit/virtus/coercion/object/class_methods/to_string_spec.rb +22 -0
- data/spec/unit/virtus/coercion/string/class_methods/to_constant_spec.rb +37 -1
- data/spec/unit/virtus/instance_methods/attributes_spec.rb +14 -2
- data/spec/unit/virtus/value_object/class_methods/allowed_writer_methods_spec.rb +15 -0
- data/spec/unit/virtus/value_object/class_methods/equalizer_spec.rb +1 -1
- data/spec/unit/virtus/value_object/initialize_spec.rb +1 -1
- data/spec/unit/virtus/value_object/with_spec.rb +1 -1
- metadata +17 -10
- data/spec/unit/virtus/attribute/default_value/instance_methods/evaluate_spec.rb +0 -30
- data/spec/unit/virtus/attribute/instance_variable_name_spec.rb +0 -12
- data/spec/unit/virtus/attribute/reader_visibility_spec.rb +0 -24
- data/spec/unit/virtus/attribute/writer_visibility_spec.rb +0 -24
- 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 [
|
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} #{
|
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 [
|
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
|
-
#{
|
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 [
|
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 [
|
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 ^ #{
|
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
|
data/lib/virtus/version.rb
CHANGED
@@ -7,10 +7,17 @@ describe "default values" do
|
|
7
7
|
class Page
|
8
8
|
include Virtus
|
9
9
|
|
10
|
-
attribute :title,
|
11
|
-
attribute :slug,
|
12
|
-
attribute :view_count,
|
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
|
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].
|
33
|
-
class_under_test.attributes[:longitude].
|
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
|
data/spec/spec_helper.rb
CHANGED
@@ -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::
|
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.
|
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
|
-
|
13
|
+
before { value.stub(:call => response) }
|
13
14
|
|
14
|
-
|
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
|
30
|
-
|
31
|
-
|
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
|
|
@@ -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, :
|
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
|