virtus 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/Gemfile +11 -15
  2. data/History.txt +10 -0
  3. data/TODO +4 -0
  4. data/VERSION +1 -1
  5. data/config/flay.yml +1 -1
  6. data/config/flog.yml +1 -1
  7. data/config/roodi.yml +6 -6
  8. data/config/site.reek +5 -5
  9. data/lib/virtus.rb +21 -11
  10. data/lib/virtus/attribute.rb +92 -59
  11. data/lib/virtus/attribute/array.rb +4 -3
  12. data/lib/virtus/attribute/boolean.rb +21 -9
  13. data/lib/virtus/attribute/date.rb +5 -3
  14. data/lib/virtus/attribute/date_time.rb +5 -3
  15. data/lib/virtus/attribute/decimal.rb +5 -3
  16. data/lib/virtus/attribute/float.rb +5 -3
  17. data/lib/virtus/attribute/hash.rb +4 -3
  18. data/lib/virtus/attribute/integer.rb +5 -3
  19. data/lib/virtus/attribute/numeric.rb +5 -3
  20. data/lib/virtus/attribute/object.rb +4 -4
  21. data/lib/virtus/attribute/string.rb +7 -9
  22. data/lib/virtus/attribute/time.rb +5 -3
  23. data/lib/virtus/attribute_set.rb +151 -0
  24. data/lib/virtus/class_methods.rb +19 -10
  25. data/lib/virtus/instance_methods.rb +51 -27
  26. data/lib/virtus/support/descendants_tracker.rb +30 -0
  27. data/lib/virtus/typecast/boolean.rb +7 -5
  28. data/lib/virtus/typecast/numeric.rb +13 -8
  29. data/lib/virtus/typecast/string.rb +24 -0
  30. data/lib/virtus/typecast/time.rb +7 -5
  31. data/spec/integration/virtus/class_methods/attribute_spec.rb +17 -5
  32. data/spec/integration/virtus/class_methods/attributes_spec.rb +2 -5
  33. data/spec/shared/idempotent_method_behaviour.rb +5 -0
  34. data/spec/spec_helper.rb +15 -0
  35. data/spec/unit/shared/attribute.rb +6 -155
  36. data/spec/unit/shared/attribute/accept_options.rb +55 -0
  37. data/spec/unit/shared/attribute/accepted_options.rb +11 -0
  38. data/spec/unit/shared/attribute/complex.rb +15 -0
  39. data/spec/unit/shared/attribute/get.rb +29 -0
  40. data/spec/unit/shared/attribute/options.rb +7 -0
  41. data/spec/unit/shared/attribute/set.rb +42 -0
  42. data/spec/unit/virtus/attribute/boolean_spec.rb +1 -2
  43. data/spec/unit/virtus/attribute/date_spec.rb +1 -2
  44. data/spec/unit/virtus/attribute/date_time_spec.rb +1 -2
  45. data/spec/unit/virtus/attribute/decimal_spec.rb +1 -2
  46. data/spec/unit/virtus/attribute/float_spec.rb +1 -2
  47. data/spec/unit/virtus/attribute/integer_spec.rb +1 -2
  48. data/spec/unit/virtus/attribute/numeric/class_methods/descendants_spec.rb +2 -2
  49. data/spec/unit/virtus/attribute/object/class_methods/descendants_spec.rb +6 -4
  50. data/spec/unit/virtus/attribute/string_spec.rb +1 -2
  51. data/spec/unit/virtus/attribute/time_spec.rb +1 -2
  52. data/spec/unit/virtus/attribute_set/append_spec.rb +35 -0
  53. data/spec/unit/virtus/attribute_set/each_spec.rb +60 -0
  54. data/spec/unit/virtus/attribute_set/element_reference_spec.rb +13 -0
  55. data/spec/unit/virtus/attribute_set/element_set_spec.rb +35 -0
  56. data/spec/unit/virtus/attribute_set/merge_spec.rb +36 -0
  57. data/spec/unit/virtus/attribute_set/parent_spec.rb +11 -0
  58. data/spec/unit/virtus/attribute_set/reset_spec.rb +60 -0
  59. data/spec/unit/virtus/class_methods/attribute_spec.rb +11 -0
  60. data/spec/unit/virtus/descendants_tracker/descendants_spec.rb +22 -0
  61. data/spec/unit/virtus/descendants_tracker/inherited_spec.rb +24 -0
  62. data/spec/unit/virtus/determine_type_spec.rb +21 -9
  63. data/spec/unit/virtus/instance_methods/{attribute_get_spec.rb → element_reference_spec.rb} +4 -2
  64. data/spec/unit/virtus/instance_methods/{attribute_set_spec.rb → element_set_spec.rb} +5 -7
  65. data/virtus.gemspec +35 -13
  66. metadata +96 -14
  67. data/lib/virtus/support/chainable.rb +0 -13
@@ -1,9 +1,11 @@
1
1
  module Virtus
2
+
2
3
  # Instance methods that are added when you include Virtus
3
4
  module InstanceMethods
5
+
4
6
  # Set attributes during initialization of an object
5
7
  #
6
- # @param [Hash] attributes
8
+ # @param [#to_hash] attributes
7
9
  # the attributes hash to be set
8
10
  #
9
11
  # @return [Object]
@@ -23,7 +25,7 @@ module Virtus
23
25
  # end
24
26
  #
25
27
  # user = User.new(:name => 'John')
26
- # user.attribute_get(:name) # => "john"
28
+ # user[:name] # => "john"
27
29
  #
28
30
  # @param [Symbol] name
29
31
  # a name of an attribute
@@ -32,8 +34,8 @@ module Virtus
32
34
  # a value of an attribute
33
35
  #
34
36
  # @api public
35
- def attribute_get(name)
36
- __send__(name)
37
+ def [](name)
38
+ attribute_get(name)
37
39
  end
38
40
 
39
41
  # Sets a value of the attribute with the given name
@@ -46,7 +48,7 @@ module Virtus
46
48
  # end
47
49
  #
48
50
  # user = User.new
49
- # user.attribute_set(:name) # => "john"
51
+ # user[:name] = "john" # => "john"
50
52
  # user.name # => "john"
51
53
  #
52
54
  # @param [Symbol] name
@@ -59,11 +61,11 @@ module Virtus
59
61
  # the value set on an object
60
62
  #
61
63
  # @api public
62
- def attribute_set(name, value)
63
- __send__("#{name}=", value)
64
+ def []=(name, value)
65
+ attribute_set(name, value)
64
66
  end
65
67
 
66
- # Mass-assign of attribute values
68
+ # Returns a hash of all publicly accessible attributes
67
69
  #
68
70
  # @example
69
71
  # class User
@@ -73,23 +75,25 @@ module Virtus
73
75
  # attribute :age, Integer
74
76
  # end
75
77
  #
76
- # user = User.new
77
- # user.attributes = { :name => 'John', :age => 28 }
78
- #
79
- # @param [Hash] attributes
80
- # a hash of attribute values to be set on an object
78
+ # user = User.new(:name => 'John', :age => 28)
79
+ # user.attributes # => { :name => 'John', :age => 28 }
81
80
  #
82
81
  # @return [Hash]
83
82
  # the attributes
84
83
  #
85
84
  # @api public
86
- def attributes=(attributes)
87
- attributes.each do |name, value|
88
- attribute_set(name, value) if respond_to?("#{name}=")
85
+ def attributes
86
+ attributes = {}
87
+
88
+ self.class.attributes.each do |attribute|
89
+ name = attribute.name
90
+ attributes[name] = attribute_get(name) if respond_to?(name)
89
91
  end
92
+
93
+ attributes
90
94
  end
91
95
 
92
- # Returns a hash of all publicly accessible attributes
96
+ # Mass-assign of attribute values
93
97
  #
94
98
  # @example
95
99
  # class User
@@ -99,21 +103,41 @@ module Virtus
99
103
  # attribute :age, Integer
100
104
  # end
101
105
  #
102
- # user = User.new(:name => 'John', :age => 28)
103
- # user.attributes # => { :name => 'John', :age => 28 }
106
+ # user = User.new
107
+ # user.attributes = { :name => 'John', :age => 28 }
108
+ #
109
+ # @param [#to_hash] attributes
110
+ # a hash of attribute values to be set on an object
104
111
  #
105
112
  # @return [Hash]
106
113
  # the attributes
107
114
  #
108
115
  # @api public
109
- def attributes
110
- attributes = {}
111
-
112
- self.class.attributes.each_key do |name|
113
- attributes[name] = attribute_get(name) if respond_to?(name)
116
+ def attributes=(attributes)
117
+ attributes.to_hash.each do |name, value|
118
+ attribute_set(name, value) if respond_to?("#{name}=")
114
119
  end
120
+ end
115
121
 
116
- attributes
122
+ private
123
+
124
+ # Returns a value of the attribute with the given name
125
+ #
126
+ # @see Virtus::InstanceMethods#[]
127
+ #
128
+ # @api private
129
+ def attribute_get(name)
130
+ __send__(name)
117
131
  end
118
- end # InstanceMethods
119
- end # Virtus
132
+
133
+ # Sets a value of the attribute with the given name
134
+ #
135
+ # @see Virtus::InstanceMethods#[]=
136
+ #
137
+ # @api private
138
+ def attribute_set(name, value)
139
+ __send__("#{name}=", value)
140
+ end
141
+
142
+ end # module InstanceMethods
143
+ end # module Virtus
@@ -0,0 +1,30 @@
1
+ module Virtus
2
+
3
+ # A module that adds descendant tracking to a class
4
+ module DescendantsTracker
5
+
6
+ # Hook called when class is inherited
7
+ #
8
+ # @param [Class] descendant
9
+ #
10
+ # @return [self]
11
+ #
12
+ # @api private
13
+ def inherited(descendant)
14
+ superclass = self.superclass
15
+ superclass.inherited(descendant) if superclass.respond_to?(:descendants)
16
+ descendants.unshift(descendant)
17
+ self
18
+ end
19
+
20
+ # Return the descendants of this class
21
+ #
22
+ # @return [Array<Class>]
23
+ #
24
+ # @api private
25
+ def descendants
26
+ @descendants ||= []
27
+ end
28
+
29
+ end # module DescendantsTracker
30
+ end # module Virtus
@@ -1,12 +1,13 @@
1
1
  module Virtus
2
2
  module Typecast
3
+
3
4
  # Typecast defined values into true or false.
4
5
  # See TRUE_VALUES and FALSE_VALUES constants for a reference.
5
6
  class Boolean
7
+
6
8
  TRUE_VALUES = [ 1, '1', 't', 'T', 'true', 'TRUE' ].freeze
7
9
  FALSE_VALUES = [ 0, '0', 'f', 'F', 'false', 'FALSE' ].freeze
8
-
9
- BOOLEAN_MAP = Hash[TRUE_VALUES.product([ true ]) + FALSE_VALUES.product([ false ]) ].freeze
10
+ BOOLEAN_MAP = Hash[ TRUE_VALUES.product([ true ]) + FALSE_VALUES.product([ false ]) ].freeze
10
11
 
11
12
  # Typecast value to TrueClass or FalseClass
12
13
  #
@@ -22,6 +23,7 @@ module Virtus
22
23
  def self.call(value)
23
24
  BOOLEAN_MAP.fetch(value, value)
24
25
  end
25
- end # Boolean
26
- end # Typecast
27
- end # Virtus
26
+
27
+ end # class Boolean
28
+ end # module Typecast
29
+ end # module Virtus
@@ -1,14 +1,18 @@
1
1
  module Virtus
2
2
  module Typecast
3
+
3
4
  # Typecast numeric values. Supports Integer, Float and BigDecimal
4
5
  class Numeric
6
+
7
+ REGEXP = /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/.freeze
8
+
5
9
  # Typecast value to integer
6
10
  #
7
11
  # @example
8
12
  # Virtus::Typecast::Numeric.to_i('1') # => 1
9
13
  # Virtus::Typecast::Numeric.to_i(1.2) # => 1
10
14
  #
11
- # @param [Object]
15
+ # @param [Object] value
12
16
  #
13
17
  # @return [Integer]
14
18
  #
@@ -23,7 +27,7 @@ module Virtus
23
27
  # Virtus::Typecast::Numeric.to_f('1.2') # => 1.2
24
28
  # Virtus::Typecast::Numeric.to_f(1) # => 1.0
25
29
  #
26
- # @param [Object]
30
+ # @param [Object] value
27
31
  #
28
32
  # @return [Float]
29
33
  #
@@ -38,7 +42,7 @@ module Virtus
38
42
  # Virtus::Typecast::Numeric.to_d('1.2') # => #<BigDecimal:b72157d4,'0.12E1',8(8)>
39
43
  # Virtus::Typecast::Numeric.to_d(1) # => #<BigDecimal:b7212e08,'0.1E1',4(8)>
40
44
  #
41
- # @param [Object]
45
+ # @param [Object] value
42
46
  #
43
47
  # @return [BigDecimal]
44
48
  #
@@ -51,7 +55,7 @@ module Virtus
51
55
  end
52
56
  end
53
57
 
54
- private
58
+ private
55
59
 
56
60
  # Match numeric string
57
61
  #
@@ -66,7 +70,7 @@ module Virtus
66
70
  # @api private
67
71
  def self.call(value, method)
68
72
  if value.respond_to?(:to_str)
69
- if value.to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
73
+ if value.to_str =~ REGEXP
70
74
  $1.send(method)
71
75
  else
72
76
  value
@@ -77,6 +81,7 @@ module Virtus
77
81
  value
78
82
  end
79
83
  end
80
- end # Numeric
81
- end # Typecast
82
- end # Virtus
84
+
85
+ end # class Numeric
86
+ end # module Typecast
87
+ end # module Virtus
@@ -0,0 +1,24 @@
1
+ module Virtus
2
+ module Typecast
3
+
4
+ # Typecast any object to a string
5
+ class String
6
+
7
+ # Typecast value to a string
8
+ #
9
+ # @example
10
+ # Virtus::Typecast::String.call(1) # => '1'
11
+ # Virtus::Typecast::String.call([]) # => '[]'
12
+ #
13
+ # @param [Object] value
14
+ #
15
+ # @return [String]
16
+ #
17
+ # @api public
18
+ def self.call(value)
19
+ value.to_s
20
+ end
21
+
22
+ end # class String
23
+ end # module Typecast
24
+ end # module Virtus
@@ -1,5 +1,6 @@
1
1
  module Virtus
2
2
  module Typecast
3
+
3
4
  # Typecast various values into Date, DateTime or Time
4
5
  class Time
5
6
  SEGMENTS = [ :year, :month, :day, :hour, :min, :sec ].freeze
@@ -67,14 +68,14 @@ module Virtus
67
68
  call(value, :to_datetime)
68
69
  end
69
70
 
70
- private
71
+ private
71
72
 
72
73
  # @api private
73
74
  def self.call(value, method)
74
75
  return value.send(method) if value.respond_to?(method)
75
76
 
76
77
  begin
77
- if value.is_a?(::Hash)
78
+ if value.kind_of?(::Hash)
78
79
  from_hash(value, method)
79
80
  else
80
81
  from_string(value.to_s, method)
@@ -157,6 +158,7 @@ module Virtus
157
158
  Numeric.to_i(value.fetch(segment, now.send(segment)))
158
159
  end
159
160
  end
160
- end # Time
161
- end # Typecast
162
- end # Virtus
161
+
162
+ end # class Time
163
+ end # module Typecast
164
+ end # module Virtus
@@ -5,9 +5,9 @@ describe Virtus::ClassMethods, '.attribute' do
5
5
  Class.new { include Virtus }
6
6
  end
7
7
 
8
- it { described_class.should respond_to(:attribute) }
8
+ specify { described_class.should respond_to(:attribute) }
9
9
 
10
- describe ".attribute" do
10
+ context "in the class" do
11
11
  before do
12
12
  described_class.attribute(:name, String)
13
13
  described_class.attribute(:email, String, :accessor => :private)
@@ -20,11 +20,11 @@ describe Virtus::ClassMethods, '.attribute' do
20
20
  let(:protected_instance_methods) { described_class.protected_instance_methods.map { |method| method.to_s } }
21
21
  let(:private_instance_methods) { described_class.private_instance_methods.map { |method| method.to_s } }
22
22
 
23
- it "should create an attribute" do
24
- described_class.attributes.should have_key(:name)
23
+ it "returns self" do
24
+ described_class.attribute(:name, String).should be(described_class)
25
25
  end
26
26
 
27
- it "should create an attribute of a correct type" do
27
+ it "creates an attribute of a correct type" do
28
28
  described_class.attributes[:name].should be_instance_of(Virtus::Attribute::String)
29
29
  end
30
30
 
@@ -60,4 +60,16 @@ describe Virtus::ClassMethods, '.attribute' do
60
60
  protected_instance_methods.should include('bday=')
61
61
  end
62
62
  end
63
+
64
+ context "in the descendants" do
65
+ subject { described_class.attribute(:name, String).attributes[:name] }
66
+
67
+ let(:descendant) { Class.new(described_class) }
68
+
69
+ it 'updates the descendant attributes' do
70
+ descendant.attributes.to_a.should be_empty
71
+ @attribute = subject
72
+ descendant.attributes.to_a.should eql([ @attribute ])
73
+ end
74
+ end
63
75
  end
@@ -15,11 +15,8 @@ describe Virtus::ClassMethods, '.attributes' do
15
15
 
16
16
  subject { described_class.attributes }
17
17
 
18
- it "returns an attributes hash" do
19
- subject.should eql(
20
- :name => described_class.attributes[:name],
21
- :age => described_class.attributes[:age]
22
- )
18
+ it "returns a set of attributes" do
19
+ subject.should be_kind_of(Virtus::AttributeSet)
23
20
  end
24
21
  end
25
22
  end
@@ -0,0 +1,5 @@
1
+ shared_examples_for 'an idempotent method' do
2
+ it 'is idempotent' do
3
+ should equal(instance_eval(&self.class.subject))
4
+ end
5
+ end
@@ -9,3 +9,18 @@ ENV['TZ'] = 'UTC'
9
9
  SPEC_ROOT = Pathname(__FILE__).dirname.expand_path
10
10
 
11
11
  Pathname.glob((SPEC_ROOT + '**/shared/**/*.rb').to_s).each { |file| require file }
12
+
13
+ RSpec.configure do |config|
14
+
15
+ # Remove anonymous Attribute classes from Attribute descendants
16
+ config.after :all do
17
+ stack = [ Virtus::Attribute ]
18
+ while klass = stack.pop
19
+ klass.descendants.delete_if do |descendant|
20
+ descendant.name.nil? || descendant.name.empty?
21
+ end
22
+ stack.concat(klass.descendants)
23
+ end
24
+ end
25
+
26
+ end
@@ -1,157 +1,8 @@
1
- # TODO: split this into separate files
2
-
3
1
  shared_examples_for "Attribute" do
4
- def attribute_name
5
- raise "+attribute_name+ should be defined"
6
- end
7
-
8
- before :all do
9
- Object.send(:remove_const, :SubAttribute) if Object.const_defined?(:SubAttribute)
10
- Object.send(:remove_const, :User) if Object.const_defined?(:User)
11
- end
12
-
13
- let(:sub_attribute) { class SubAttribute < described_class; end; SubAttribute }
14
-
15
- let(:model) do
16
- Class.new { include Virtus }
17
- end
18
-
19
- describe ".options" do
20
- subject { described_class.options }
21
- it { should be_instance_of(Hash) }
22
- end
23
-
24
- describe ".accepted_options" do
25
- it "returns an array of accepted options" do
26
- described_class.accepted_options.should be_instance_of(Array)
27
- end
28
-
29
- it "accepts base options" do
30
- described_class.accepted_options.should include(*Virtus::Attribute::OPTIONS)
31
- end
32
- end
33
-
34
- describe ".accept_options" do
35
- let(:new_option) { :width }
36
-
37
- before :all do
38
- described_class.accepted_options.should_not include(new_option)
39
- described_class.accept_options(new_option)
40
- end
41
-
42
- it "sets new accepted options on itself" do
43
- described_class.accepted_options.should include(new_option)
44
- end
45
-
46
- it "sets new accepted option on its descendants" do
47
- sub_attribute.accepted_options.should include(new_option)
48
- end
49
-
50
- it "creates option accessors" do
51
- described_class.should respond_to(new_option)
52
- end
53
-
54
- it "creates option accessors on descendants" do
55
- sub_attribute.should respond_to(new_option)
56
- end
57
-
58
- context 'with default option value' do
59
- let(:option) { :height }
60
- let(:value) { 10 }
61
-
62
- before :all do
63
- sub_attribute.accept_options(option)
64
- sub_attribute.height(value)
65
- end
66
-
67
- context "when new attribute is created" do
68
- subject { sub_attribute.new(attribute_name) }
69
-
70
- it "sets the default value" do
71
- subject.options[option].should eql(value)
72
- end
73
- end
74
-
75
- context "when new attribute is created and overrides option's default value" do
76
- let(:new_value) { 11 }
77
-
78
- subject { sub_attribute.new(attribute_name, option => new_value) }
79
-
80
- it "sets the new value" do
81
- subject.options[option].should eql(new_value)
82
- end
83
- end
84
- end
85
- end
86
-
87
- describe "#set" do
88
- let(:attribute) { model.attribute(attribute_name, described_class) }
89
- let(:object) { model.new }
90
-
91
- context "with nil" do
92
- subject { attribute.set(object, nil) }
93
-
94
- it "doesn't set the ivar" do
95
- subject
96
- object.instance_variable_defined?(attribute.instance_variable_name).should be(false)
97
- end
98
-
99
- it "returns nil" do
100
- subject.should be(nil)
101
- end
102
- end
103
-
104
- context "with a primitive value" do
105
- before { attribute.set(object, attribute_value) }
106
-
107
- it "sets the value in an ivar" do
108
- object.instance_variable_get(attribute.instance_variable_name).should eql(attribute_value)
109
- end
110
- end
111
-
112
- context "with a non-primitive value" do
113
- before { attribute.set(object, attribute_value_other) }
114
-
115
- it "sets the value in an ivar converted to the primitive type" do
116
- object.instance_variable_get(attribute.instance_variable_name).should be_kind_of(described_class.primitive)
117
- end
118
- end
119
- end
120
-
121
- describe "#get" do
122
- let(:attribute) { model.attribute(attribute_name, described_class) }
123
- let(:object) { model.new }
124
-
125
- context "when a non-nil value is set" do
126
- before { attribute.set(object, attribute_value) }
127
-
128
- subject { attribute.get(object) }
129
-
130
- it { should eql(attribute_value) }
131
- end
132
-
133
- context "when nil is set" do
134
- before { attribute.set(object, nil) }
135
-
136
- subject { attribute.get(object) }
137
-
138
- it { should be(nil) }
139
- end
140
- end
141
-
142
- describe "#complex?" do
143
- let(:attribute) { model.attribute(attribute_name, described_class, :complex => complex) }
144
-
145
- subject { attribute.complex? }
146
-
147
- context "when set to true" do
148
- let(:complex) { true }
149
- it { should be(true) }
150
- end
151
-
152
- context "when set to false" do
153
- let(:complex) { false }
154
- it { should be(false) }
155
- end
156
- end
2
+ it_behaves_like 'Attribute.options'
3
+ it_behaves_like 'Attribute.accept_options'
4
+ it_behaves_like 'Attribute.accepted_options'
5
+ it_behaves_like 'Attribute#set'
6
+ it_behaves_like 'Attribute#get'
7
+ it_behaves_like 'Attribute#complex?'
157
8
  end