virtus 0.0.3 → 0.0.4

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 (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