virtus 0.0.5 → 0.0.6

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 (113) hide show
  1. data/.travis.yml +9 -2
  2. data/.yardopts +1 -0
  3. data/History.md +51 -0
  4. data/{README.markdown → README.md} +63 -7
  5. data/TODO +2 -4
  6. data/VERSION +1 -1
  7. data/config/flay.yml +2 -2
  8. data/config/flog.yml +1 -1
  9. data/config/roodi.yml +5 -6
  10. data/config/site.reek +3 -3
  11. data/examples/custom_coercion_spec.rb +50 -0
  12. data/examples/default_values_spec.rb +21 -0
  13. data/lib/virtus.rb +21 -6
  14. data/lib/virtus/attribute.rb +113 -253
  15. data/lib/virtus/attribute/array.rb +6 -3
  16. data/lib/virtus/attribute/boolean.rb +9 -28
  17. data/lib/virtus/attribute/date.rb +9 -12
  18. data/lib/virtus/attribute/date_time.rb +10 -12
  19. data/lib/virtus/attribute/decimal.rb +4 -11
  20. data/lib/virtus/attribute/float.rb +4 -11
  21. data/lib/virtus/attribute/hash.rb +5 -3
  22. data/lib/virtus/attribute/integer.rb +4 -11
  23. data/lib/virtus/attribute/numeric.rb +1 -0
  24. data/lib/virtus/attribute/object.rb +1 -0
  25. data/lib/virtus/attribute/string.rb +4 -11
  26. data/lib/virtus/attribute/time.rb +9 -16
  27. data/lib/virtus/class_methods.rb +42 -7
  28. data/lib/virtus/coercion.rb +32 -0
  29. data/lib/virtus/coercion/date.rb +26 -0
  30. data/lib/virtus/coercion/date_time.rb +26 -0
  31. data/lib/virtus/coercion/decimal.rb +40 -0
  32. data/lib/virtus/coercion/false_class.rb +24 -0
  33. data/lib/virtus/coercion/float.rb +24 -0
  34. data/lib/virtus/coercion/hash.rb +82 -0
  35. data/lib/virtus/coercion/integer.rb +60 -0
  36. data/lib/virtus/coercion/numeric.rb +66 -0
  37. data/lib/virtus/coercion/object.rb +25 -0
  38. data/lib/virtus/coercion/string.rb +155 -0
  39. data/lib/virtus/coercion/symbol.rb +24 -0
  40. data/lib/virtus/coercion/time.rb +26 -0
  41. data/lib/virtus/coercion/time_coercions.rb +85 -0
  42. data/lib/virtus/coercion/true_class.rb +24 -0
  43. data/lib/virtus/instance_methods.rb +7 -0
  44. data/lib/virtus/support/descendants_tracker.rb +1 -1
  45. data/lib/virtus/support/options.rb +114 -0
  46. data/lib/virtus/support/type_lookup.rb +95 -0
  47. data/spec/integration/virtus/attributes/attribute/{typecast_spec.rb → set_spec.rb} +7 -7
  48. data/spec/unit/shared/attribute.rb +3 -3
  49. data/spec/unit/shared/attribute/accept_options.rb +0 -18
  50. data/spec/unit/shared/attribute/accepted_options.rb +0 -6
  51. data/spec/unit/shared/attribute/get.rb +32 -17
  52. data/spec/unit/shared/attribute/inspect.rb +7 -0
  53. data/spec/unit/shared/attribute/primitive.rb +15 -0
  54. data/spec/unit/shared/attribute/set.rb +16 -21
  55. data/spec/unit/virtus/attribute/array_spec.rb +18 -3
  56. data/spec/unit/virtus/attribute/boolean_spec.rb +8 -6
  57. data/spec/unit/virtus/attribute/date_spec.rb +8 -6
  58. data/spec/unit/virtus/attribute/date_time_spec.rb +8 -6
  59. data/spec/unit/virtus/attribute/decimal_spec.rb +18 -6
  60. data/spec/unit/virtus/attribute/float_spec.rb +19 -7
  61. data/spec/unit/virtus/attribute/hash_spec.rb +5 -3
  62. data/spec/unit/virtus/attribute/integer_spec.rb +10 -8
  63. data/spec/unit/virtus/attribute/string_spec.rb +10 -8
  64. data/spec/unit/virtus/attribute/time_spec.rb +8 -6
  65. data/spec/unit/virtus/class_methods/attributes_spec.rb +11 -0
  66. data/spec/unit/virtus/coercion/class_name_reference_spec.rb +17 -0
  67. data/spec/unit/virtus/coercion/date/class_methods/to_datetime_spec.rb +30 -0
  68. data/spec/unit/virtus/coercion/date/class_methods/to_string_spec.rb +12 -0
  69. data/spec/unit/virtus/coercion/date/class_methods/to_time_spec.rb +12 -0
  70. data/spec/unit/virtus/coercion/date_time/class_methods/to_date_spec.rb +30 -0
  71. data/spec/unit/virtus/coercion/date_time/class_methods/to_string_spec.rb +12 -0
  72. data/spec/unit/virtus/coercion/date_time/class_methods/to_time_spec.rb +30 -0
  73. data/spec/unit/virtus/coercion/decimal/class_methods/to_float_spec.rb +12 -0
  74. data/spec/unit/virtus/coercion/decimal/class_methods/to_integer_spec.rb +12 -0
  75. data/spec/unit/virtus/coercion/decimal/class_methods/to_string_spec.rb +12 -0
  76. data/spec/unit/virtus/coercion/false_class/class_methods/to_string_spec.rb +12 -0
  77. data/spec/unit/virtus/coercion/float/class_methods/to_decimal_spec.rb +12 -0
  78. data/spec/unit/virtus/coercion/float/class_methods/to_integer_spec.rb +12 -0
  79. data/spec/unit/virtus/coercion/float/class_methods/to_string_spec.rb +12 -0
  80. data/spec/unit/virtus/coercion/hash/class_methods/to_array_spec.rb +12 -0
  81. data/spec/unit/virtus/coercion/hash/class_methods/to_date_spec.rb +31 -0
  82. data/spec/unit/virtus/coercion/hash/class_methods/to_datetime_spec.rb +31 -0
  83. data/spec/unit/virtus/coercion/hash/class_methods/to_time_spec.rb +31 -0
  84. data/spec/unit/virtus/coercion/integer/class_methods/to_boolean_spec.rb +25 -0
  85. data/spec/unit/virtus/coercion/integer/class_methods/to_decimal_spec.rb +12 -0
  86. data/spec/unit/virtus/coercion/integer/class_methods/to_float_spec.rb +12 -0
  87. data/spec/unit/virtus/coercion/integer/class_methods/to_string_spec.rb +12 -0
  88. data/spec/unit/virtus/coercion/object/class_methods/method_missing_spec.rb +33 -0
  89. data/spec/unit/virtus/coercion/string/class_methods/to_boolean_spec.rb +29 -0
  90. data/spec/unit/virtus/coercion/string/class_methods/to_date_spec.rb +23 -0
  91. data/spec/unit/virtus/coercion/string/class_methods/to_datetime_spec.rb +50 -0
  92. data/spec/unit/virtus/coercion/string/class_methods/to_decimal_spec.rb +23 -0
  93. data/spec/unit/virtus/coercion/string/class_methods/to_float_spec.rb +21 -0
  94. data/spec/unit/virtus/coercion/string/class_methods/to_integer_spec.rb +21 -0
  95. data/spec/unit/virtus/coercion/string/class_methods/to_time_spec.rb +50 -0
  96. data/spec/unit/virtus/coercion/symbol/class_methods/to_string_spec.rb +12 -0
  97. data/spec/unit/virtus/coercion/true_class/class_methods/to_string_spec.rb +12 -0
  98. data/spec/unit/virtus/instance_methods/attributes_spec.rb +7 -0
  99. data/spec/unit/virtus/options/accept_options_spec.rb +38 -0
  100. data/spec/unit/virtus/options/accepted_options_spec.rb +21 -0
  101. data/spec/unit/virtus/options/options_spec.rb +11 -0
  102. data/spec/unit/virtus/type_lookup/determine_type_spec.rb +68 -0
  103. data/spec/unit/virtus/type_lookup/primitive_spec.rb +9 -0
  104. data/virtus.gemspec +70 -17
  105. metadata +78 -27
  106. data/History.txt +0 -38
  107. data/lib/virtus/typecast/boolean.rb +0 -29
  108. data/lib/virtus/typecast/numeric.rb +0 -87
  109. data/lib/virtus/typecast/string.rb +0 -24
  110. data/lib/virtus/typecast/time.rb +0 -192
  111. data/spec/unit/shared/attribute/complex.rb +0 -15
  112. data/spec/unit/shared/attribute/options.rb +0 -7
  113. data/spec/unit/virtus/attribute/attribute_spec.rb +0 -12
data/.travis.yml CHANGED
@@ -1,6 +1,13 @@
1
- script: "bundle exec rspec spec/"
1
+ bundler_args: --without guard metrics
2
+ script: "bundle exec rake spec"
2
3
  rvm:
3
4
  - 1.8.7
4
5
  - 1.9.2
5
- - jruby
6
6
  - rbx
7
+ - rbx-2.0
8
+ - ree
9
+ - jruby
10
+ - ruby-head
11
+ notifications:
12
+ email:
13
+ - piotr.solnica@gmail.com
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ - README.md History.md LICENSE
data/History.md ADDED
@@ -0,0 +1,51 @@
1
+ # v0.0.6 to-be-released
2
+
3
+ * [BREAKING CHANGE] Moved Virtus.determine_type to a shared module Virtus::TypeLookup (dkubb)
4
+ * [BREAKING CHANGE] Attribute#typecast_to_primitive has been replaced by Attribute#coerce (solnic)
5
+ * [BREAKING CHANGE] Attribute#typecast logic was moved to Attribute#set which is now a public method (solnic)
6
+ * [feature] Added support for default values (solnic)
7
+ * [general] Added custom inspect for Attribute classes (solnic)
8
+ * [general] Added backports as a development dependency (dkubb)
9
+ * [changed] Options API has been extracted from Attribute to a support module Virtus::Options (solnic)
10
+ * [changed] Typecast classes have been replace by a new hierarchy of Coercion classes like Coercion::String, Coercion::Integer etc. (solnic)
11
+ * [changed] Attribute#get, #get!, #set, #set! & #coerce are now part of the public API (solnic)
12
+
13
+ [Compare v0.0.5..master](https://github.com/solnic/virtus/compare/v0.0.5...master)
14
+
15
+ # v0.0.5 2011-07-10
16
+
17
+ * [bugfix] Fixed DescendantsTracker + ActiveSupport collision (dkubb)
18
+
19
+ [Compare v0.0.4..v0.0.5](https://github.com/solnic/virtus/compare/v0.0.4...v0.0.5)
20
+
21
+ # v0.0.4 2011-07-08
22
+
23
+ * [BREAKING CHANGE] attributes hash has been replaced by a specialized class AttributeSet (dkubb)
24
+ * [BREAKING CHANGE] Virtus::ClassMethods.attribute returns self instead of a created attribute (solnic)
25
+ * [changed] descendants tracking has been extracted into DescendantsTracker module (dkubb)
26
+ * [changed] Instance #primitive? method has been replaced by class utility method Virtus::Attribute.primitive? (solnic)
27
+ * [changed] Virtus::Attribute::String#typecast_to_primitive delegates to Virtus::Typecast::String.call (solnic)
28
+
29
+ [Compare v0.0.3..v0.0.4](https://github.com/solnic/virtus/compare/v0.0.3...v0.0.4)
30
+
31
+ # v0.0.3 2011-06-09
32
+
33
+ * [BREAKING CHANGE] Attribute classes were moved to Virtus::Attribute namespace (solnic)
34
+ * [BREAKING CHANGE] Attribute instance no longer holds the reference to a model (solnic)
35
+ * [BREAKING CHANGE] #typecast no longer receives an instance of a model (override #set which calls #typecast if you need that) (solnic)
36
+ * [changed] Adding reader/writer methods was moved from the attribute constructor to Virtus::ClassMethods.attribute (solnic)
37
+ * [changed] Typecast logic has been moved into separate classes (see Virtus::Typecast) (solnic)
38
+ * [added] Virtus::Attribute::DateTime#typecast supports objects which implement #to_datetime (solnic)
39
+ * [general] Internals have been cleaned up, simplified and properly documented (solnic)
40
+
41
+ [Compare v0.0.2..v0.0.3](https://github.com/solnic/virtus/compare/v0.0.2...v0.0.3)
42
+
43
+ # v0.0.2 2011-06-06
44
+
45
+ * [bugfix] Fixed #typecast in custom attribute classes (solnic)
46
+
47
+ [Compare v0.0.1..v0.0.2](https://github.com/solnic/virtus/compare/v0.0.1...v0.0.2)
48
+
49
+ # v0.0.1 2011-06-04
50
+
51
+ First public release :)
@@ -2,11 +2,11 @@
2
2
 
3
3
  This is a partial extraction of the DataMapper [Property
4
4
  API](http://rubydoc.info/github/datamapper/dm-core/master/DataMapper/Property)
5
- with various modifications. My goal is to provide a common API to define
6
- attributes on a model along with (auto-)validations so all ORMs/ODMs could use
7
- it instead of reinventing the wheel all over again. It would be also suitable
8
- for any other usecase where you need to extend your ruby objects with various
9
- attributes that require typecasting and/or validations.
5
+ with various modifications and improvements. The goal is to provide a common API
6
+ for defining attributes on a model so all ORMs/ODMs could use it instead of
7
+ reinventing the wheel all over again. It is also suitable for any other
8
+ usecase where you need to extend your ruby objects with attributes that require
9
+ data type coercions.
10
10
 
11
11
  ## Installation
12
12
 
@@ -33,13 +33,63 @@ attributes that require typecasting and/or validations.
33
33
  # hash of attributes
34
34
  user.attributes # => { :name => "Piotr" }
35
35
 
36
- # automatic typecasting
36
+ # automatic coercion
37
37
  user.age = '28'
38
38
  user.age # => 28
39
39
 
40
40
  user.birthday = 'November 18th, 1983'
41
41
  user.birthday # => #<DateTime: 1983-11-18T00:00:00+00:00 (4891313/2,0/1,2299161)>
42
42
 
43
+ ## Default values
44
+
45
+ require 'virtus'
46
+
47
+ class Page
48
+ include Virtus
49
+
50
+ attribute :title, String
51
+ attribute :slug, String, :default => lambda { |post, attribute| post.title.downcase.gsub(' ', '-') }
52
+ attribute :view_count, Integer, :default => 0
53
+ end
54
+
55
+ page = Page.new(:title => 'Virtus Is Awesome')
56
+ page.slug # => 'virtus-is-awesome'
57
+ page.view_count # => 0
58
+
59
+ ## Coercions
60
+
61
+ Virtus comes with a builtin coercion library. It's super easy to add your own
62
+ coercion classes. Take a look:
63
+
64
+ require 'virtus'
65
+ require 'digest/md5'
66
+
67
+ class MD5 < Virtus::Attribute::Object
68
+ primitive String
69
+ coercion_method :to_md5
70
+ end
71
+
72
+ module Virtus
73
+ class Coercion
74
+ class String < Virtus::Coercion::Object
75
+ def self.to_md5(value)
76
+ Digest::MD5.hexdigest(value)
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ class User
83
+ include Virtus
84
+
85
+ attribute :name, String
86
+ attribute :password, MD5
87
+ end
88
+
89
+ user = User.new(:name => 'Piotr', :password => 'foobar')
90
+ user.name # => 'Piotr'
91
+ user.password # => '3858f62230ac3c915f300c664312c63f'
92
+
43
93
  ## Custom Attributes
44
94
 
45
95
  require 'virtus'
@@ -50,7 +100,7 @@ attributes that require typecasting and/or validations.
50
100
  class JSON < Virtus::Attribute::Object
51
101
  primitive Hash
52
102
 
53
- def typecast(value)
103
+ def coerce(value)
54
104
  ::JSON.parse(value)
55
105
  end
56
106
  end
@@ -68,6 +118,12 @@ attributes that require typecasting and/or validations.
68
118
  user.info = '{"email":"john@domain.com"}'
69
119
  user.info # => {"email"=>"john@domain.com"}
70
120
 
121
+ ## Contributors
122
+
123
+ * Dan Kubb ([dkubb](https://github.com/dkubb))
124
+ * Chris Corbyn ([d11wtq](https://github.com/d11wtq))
125
+ * Emmanuel Gomez ([emmanuel](https://github.com/emmanuel))
126
+
71
127
  ## Note on Patches/Pull Requests
72
128
 
73
129
  * Fork the project.
data/TODO CHANGED
@@ -1,4 +1,2 @@
1
- * Add Virtus::Typecast::Array
2
- * Add Virtus::Typecast::Hash
3
- * Add support for default values
4
- * Add better inspection of attribute instances
1
+ * Make #to_time #to_date and #to_datetime work on Ruby 1.8.7 instead of typecasting to string and parsing the value
2
+ * Add support for defining attributes on Modules
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.5
1
+ 0.0.6
data/config/flay.yml CHANGED
@@ -1,3 +1,3 @@
1
1
  ---
2
- threshold: 19.0
3
- total_score: 187
2
+ threshold: 20
3
+ total_score: 268
data/config/flog.yml CHANGED
@@ -1,2 +1,2 @@
1
1
  ---
2
- threshold: 17.2
2
+ threshold: 15.9
data/config/roodi.yml CHANGED
@@ -1,18 +1,17 @@
1
1
  ---
2
- AbcMetricMethodCheck: { score: 9.5 }
2
+ AbcMetricMethodCheck: { score: 8.1 }
3
3
  AssignmentInConditionalCheck: { }
4
4
  CaseMissingElseCheck: { }
5
- ClassLineCountCheck: { line_count: 404 }
5
+ ClassLineCountCheck: { line_count: 309 }
6
6
  ClassNameCheck: { pattern: !ruby/regexp /\A(?:[A-Z]+|[A-Z][a-z](?:[A-Z]?[a-z])+)\z/ }
7
7
  ClassVariableCheck: { }
8
8
  CyclomaticComplexityBlockCheck: { complexity: 2 }
9
- CyclomaticComplexityMethodCheck: { complexity: 3 }
9
+ CyclomaticComplexityMethodCheck: { complexity: 4 }
10
10
  EmptyRescueBodyCheck: { }
11
11
  ForLoopCheck: { }
12
- # TODO: decrease line_count to 5 to 10
13
- MethodLineCountCheck: { line_count: 19 }
12
+ MethodLineCountCheck: { line_count: 9 }
14
13
  MethodNameCheck: { pattern: !ruby/regexp /\A(?:[a-z\d](?:_?[a-z\d])+[?!=]?|\[\]=?|==|<=>|<<|[+*&|-])\z/ }
15
- ModuleLineCountCheck: { line_count: 410 }
14
+ ModuleLineCountCheck: { line_count: 315 }
16
15
  ModuleNameCheck: { pattern: !ruby/regexp /\A(?:[A-Z]+|[A-Z][a-z](?:[A-Z]?[a-z])+)\z/ }
17
16
  # TODO: decrease parameter_count to 2 or less
18
17
  ParameterNumberCheck: { parameter_count: 3 }
data/config/site.reek CHANGED
@@ -11,7 +11,7 @@ LargeClass:
11
11
  max_methods: 11
12
12
  exclude: []
13
13
  enabled: true
14
- max_instance_variables: 8
14
+ max_instance_variables: 7
15
15
  UncommunicativeMethodName:
16
16
  accept: []
17
17
  exclude: []
@@ -21,12 +21,12 @@ UncommunicativeMethodName:
21
21
  - !ruby/regexp /[0-9]$/
22
22
  - !ruby/regexp /[A-Z]/
23
23
  LongParameterList:
24
- max_params: 3 # TODO: decrease max_params to 2
24
+ max_params: 3
25
25
  exclude: []
26
26
  enabled: true
27
27
  overrides: {}
28
28
  FeatureEnvy:
29
- exclude: []
29
+ exclude: [Virtus::Coercion::TimeCoercions#to_string, Virtus::Coercion::TimeCoercions#coerce_with_method]
30
30
  enabled: true
31
31
  ClassVariable:
32
32
  exclude: []
@@ -0,0 +1,50 @@
1
+ require './spec/spec_helper'
2
+
3
+ class MD5 < Virtus::Attribute::Object
4
+ primitive String
5
+ coercion_method :to_md5
6
+ end
7
+
8
+ module Virtus
9
+ class Coercion
10
+ class String < Virtus::Coercion::Object
11
+ def self.to_md5(value)
12
+ Digest::MD5.hexdigest(value)
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ class User
19
+ include Virtus
20
+
21
+ attribute :name, String
22
+ attribute :password, MD5
23
+ end
24
+
25
+ describe User do
26
+ it { should respond_to(:name) }
27
+ it { should respond_to(:name=) }
28
+ it { should respond_to(:password) }
29
+ it { should respond_to(:password=) }
30
+
31
+ describe '#name=' do
32
+ let(:value) { 'Piotr' }
33
+
34
+ before do
35
+ subject.name = value
36
+ end
37
+
38
+ its(:name) { should == value }
39
+ end
40
+
41
+ describe '#password=' do
42
+ let(:value) { 'foobar' }
43
+
44
+ before do
45
+ subject.password = value
46
+ end
47
+
48
+ its(:password) { should == Virtus::Coercion::String.to_md5(value) }
49
+ end
50
+ end
@@ -0,0 +1,21 @@
1
+ require './spec/spec_helper'
2
+
3
+ class Page
4
+ include Virtus
5
+
6
+ attribute :title, String
7
+ attribute :slug, String, :default => lambda { |post, attribute| post.title.downcase.gsub(' ', '-') }
8
+ attribute :view_count, Integer, :default => 0
9
+ end
10
+
11
+ describe Page do
12
+ describe '#slug' do
13
+ before { subject.title = 'Virtus Is Awesome' }
14
+
15
+ its(:slug) { should eql('virtus-is-awesome') }
16
+ end
17
+
18
+ describe '#views_count' do
19
+ its(:view_count) { should eql(0) }
20
+ end
21
+ end
data/lib/virtus.rb CHANGED
@@ -13,28 +13,43 @@ module Virtus
13
13
  #
14
14
  # @param [Class] descendant
15
15
  #
16
- # @return [Class]
16
+ # @return [undefined]
17
17
  #
18
18
  # @api private
19
19
  def self.included(descendant)
20
- descendant.extend(DescendantsTracker)
20
+ super
21
21
  descendant.extend(ClassMethods)
22
22
  descendant.send(:include, InstanceMethods)
23
23
  end
24
24
 
25
+ private_class_method :included
26
+
25
27
  end # module Virtus
26
28
 
27
29
  require 'virtus/support/descendants_tracker'
30
+ require 'virtus/support/type_lookup'
31
+ require 'virtus/support/options'
28
32
 
29
33
  require 'virtus/class_methods'
30
34
  require 'virtus/instance_methods'
31
35
 
32
36
  require 'virtus/attribute_set'
33
37
 
34
- require 'virtus/typecast/boolean'
35
- require 'virtus/typecast/numeric'
36
- require 'virtus/typecast/string'
37
- require 'virtus/typecast/time'
38
+ require 'virtus/coercion'
39
+ require 'virtus/coercion/object'
40
+ require 'virtus/coercion/numeric'
41
+ require 'virtus/coercion/integer'
42
+ require 'virtus/coercion/float'
43
+ require 'virtus/coercion/decimal'
44
+ require 'virtus/coercion/false_class'
45
+ require 'virtus/coercion/true_class'
46
+ require 'virtus/coercion/hash'
47
+ require 'virtus/coercion/time_coercions'
48
+ require 'virtus/coercion/date'
49
+ require 'virtus/coercion/date_time'
50
+ require 'virtus/coercion/time'
51
+ require 'virtus/coercion/string'
52
+ require 'virtus/coercion/symbol'
38
53
 
39
54
  require 'virtus/attribute'
40
55
  require 'virtus/attribute/object'
@@ -5,185 +5,8 @@ module Virtus
5
5
  # @abstract
6
6
  class Attribute
7
7
  extend DescendantsTracker
8
-
9
- # Returns a Virtus::Attribute::Object descendant based on a name or class
10
- #
11
- # @example
12
- # Attribute.determine_type('String') # => Virtus::Attribute::String
13
- #
14
- # @param [Class, #to_s] class_or_name
15
- # name of a class or a class itself
16
- #
17
- # @return [Class]
18
- # one of the Virtus::Attribute::Object descendants
19
- #
20
- # @return [nil]
21
- # nil if the type cannot be determined by the class_or_name
22
- #
23
- # @api public
24
- def self.determine_type(class_or_name)
25
- # first match on the Attribute singleton class first, then match
26
- # any class, finally fallback to matching on the string
27
- case class_or_name
28
- when Attribute::Object.singleton_class then determine_type_from_attribute(class_or_name)
29
- when Class then determine_type_from_primitive(class_or_name)
30
- else
31
- determine_type_from_string(class_or_name.to_s)
32
- end
33
- end
34
-
35
- # Return the Attribute class given an Attribute descendant
36
- #
37
- # @param [Class<Attribute>] attribute
38
- #
39
- # @return [Class<Attribute>]
40
- #
41
- # @api private
42
- def self.determine_type_from_attribute(attribute)
43
- attribute
44
- end
45
-
46
- private_class_method :determine_type_from_attribute
47
-
48
- # Return the Attribute class given a primitive
49
- #
50
- # @param [Class] primitive
51
- #
52
- # @return [Class<Attribute>]
53
- #
54
- # @return [nil]
55
- # nil if the type cannot be determined by the primitive
56
- #
57
- # @api private
58
- def self.determine_type_from_primitive(primitive)
59
- descendants.detect { |descendant| primitive <= descendant.primitive }
60
- end
61
-
62
- private_class_method :determine_type_from_primitive
63
-
64
- # Return the Attribute class given a string
65
- #
66
- # @param [String] string
67
- #
68
- # @return [Class<Attribute>]
69
- #
70
- # @return [nil]
71
- # nil if the type cannot be determined by the string
72
- #
73
- # @api private
74
- def self.determine_type_from_string(string)
75
- const_get(string) if const_defined?(string)
76
- end
77
-
78
- private_class_method :determine_type_from_string
79
-
80
- # Returns default options hash for a given attribute class
81
- #
82
- # @example
83
- # Virtus::Attribute::String.options
84
- # # => {:primitive => String, :complex => false}
85
- #
86
- # @return [Hash]
87
- # a hash of default option values
88
- #
89
- # @api public
90
- def self.options
91
- options = {}
92
- accepted_options.each do |option_name|
93
- option_value = send(option_name)
94
- options[option_name] = option_value unless option_value.nil?
95
- end
96
- options
97
- end
98
-
99
- # Returns an array of valid options
100
- #
101
- # @example
102
- # Virtus::Attribute::String.accepted_options
103
- # # => [:primitive, :complex, :accessor, :reader, :writer]
104
- #
105
- # @return [Array]
106
- # the array of valid option names
107
- #
108
- # @api public
109
- def self.accepted_options
110
- @accepted_options ||= []
111
- end
112
-
113
- # Defines which options are valid for a given attribute class
114
- #
115
- # @example
116
- # class MyAttribute < Virtus::Attribute::Object
117
- # accept_options :foo, :bar
118
- # end
119
- #
120
- # @return [Array]
121
- # All accepted options
122
- #
123
- # @api public
124
- def self.accept_options(*new_options)
125
- add_accepted_options(new_options)
126
- new_options.each { |option| add_option_method(option) }
127
- descendants.each { |descendant| descendant.add_accepted_options(new_options) }
128
- self
129
- end
130
-
131
- # Adds a reader/writer method for the give option name
132
- #
133
- # @return [undefined]
134
- #
135
- # @api private
136
- def self.add_option_method(option)
137
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
138
- def self.#{option}(value = Undefined) # def self.primitive(value = Undefined)
139
- return @#{option} if value.equal?(Undefined) # return @primitive if value.equal?(Undefined)
140
- @#{option} = value # @primitive = value
141
- end # end
142
- RUBY
143
- end
144
-
145
- private_class_method :add_option_method
146
-
147
- # Sets default options
148
- #
149
- # @param [#to_hash] new_options
150
- # options to be set
151
- #
152
- # @return [self]
153
- #
154
- # @api private
155
- def self.set_options(new_options)
156
- new_options.to_hash.each do |option_name, option_value|
157
- send(option_name, option_value)
158
- end
159
- self
160
- end
161
-
162
- # Adds new options that an attribute class can accept
163
- #
164
- # @param [#to_ary] new_options
165
- # new options to be added
166
- #
167
- # @return [self]
168
- #
169
- # @api private
170
- def self.add_accepted_options(new_options)
171
- accepted_options.concat(new_options.to_ary)
172
- self
173
- end
174
-
175
- # Adds descendant to descendants array and inherits default options
176
- #
177
- # @param [Class] descendant
178
- #
179
- # @return [self]
180
- #
181
- # @api private
182
- def self.inherited(descendant)
183
- super
184
- descendant.add_accepted_options(accepted_options).set_options(options)
185
- self
186
- end
8
+ extend TypeLookup
9
+ extend Options
187
10
 
188
11
  # Returns if the given value's class is an attribute's primitive
189
12
  #
@@ -235,9 +58,24 @@ module Virtus
235
58
  # @api private
236
59
  attr_reader :writer_visibility
237
60
 
61
+ # Returns method name that should be used for coerceing
62
+ #
63
+ # @return [Symbol]
64
+ #
65
+ # @api private
66
+ attr_reader :coercion_method
67
+
68
+ # Returns default value
69
+ #
70
+ # @return [Object]
71
+ #
72
+ # @api private
73
+ attr_reader :default
74
+
238
75
  DEFAULT_ACCESSOR = :public
239
76
 
240
- OPTIONS = [ :primitive, :complex, :accessor, :reader, :writer ].freeze
77
+ OPTIONS = [ :primitive, :accessor, :reader,
78
+ :writer, :coercion_method, :default ].freeze
241
79
 
242
80
  accept_options *OPTIONS
243
81
 
@@ -257,140 +95,146 @@ module Virtus
257
95
  @options = self.class.options.merge(options.to_hash).freeze
258
96
 
259
97
  @instance_variable_name = "@#{@name}".freeze
260
- @complex = @options.fetch(:complex, false)
98
+ @coercion_method = @options.fetch(:coercion_method)
261
99
 
100
+ set_default
262
101
  set_visibility
263
102
  end
264
103
 
265
- # Returns if an attribute is a complex one
104
+ # Returns a concise string representation of the attribute instance
266
105
  #
267
106
  # @example
268
- # Virtus::Attribute::String.complex? # => false
269
- # Virtus::Attribute::Array.complex? # => true
107
+ # attribute = Virtus::Attribute::String.new(:name)
108
+ # attribute.inspect # => #<Virtus::Attribute::String @name=:name>
270
109
  #
271
- # @return [Boolean]
110
+ # @return [String]
272
111
  #
273
112
  # @api public
274
- def complex?
275
- @complex
276
- end
277
-
278
- # Converts the given value to the primitive type
279
- #
280
- # @param [Object] value
281
- # the value
282
- #
283
- # @return [Object]
284
- # nil, original value or value converted to the primitive type
285
- #
286
- # @api private
287
- def typecast(value)
288
- if value.nil? || self.class.primitive?(value)
289
- value
290
- else
291
- typecast_to_primitive(value)
292
- end
113
+ def inspect
114
+ "#<#{self.class.name} @name=#{name.inspect}>"
293
115
  end
294
116
 
295
- # Converts the given value to the primitive type
117
+ # Returns value of an attribute for the given instance
296
118
  #
297
- # @return [Object]
119
+ # Sets the default value if an ivar is not set and default
120
+ # value is configured
298
121
  #
299
- # @api private
300
- def typecast_to_primitive(value)
301
- value
302
- end
303
-
304
- # Returns value of an attribute for the given instance
122
+ # @example
123
+ # attribute.get(instance) # => value
305
124
  #
306
125
  # @return [Object]
307
126
  # value of an attribute
308
127
  #
309
- # @api private
128
+ # @api public
310
129
  def get(instance)
311
- get!(instance)
130
+ if instance.instance_variable_defined?(instance_variable_name)
131
+ get!(instance)
132
+ else
133
+ set_default_value(instance)
134
+ end
312
135
  end
313
136
 
314
137
  # Returns the instance variable of the attribute
315
138
  #
139
+ # @example
140
+ # attribute.get!(instance) # => value
141
+ #
316
142
  # @return [Object]
317
143
  # value of an attribute
318
144
  #
319
- # @api private
145
+ # @api public
320
146
  def get!(instance)
321
147
  instance.instance_variable_get(instance_variable_name)
322
148
  end
323
149
 
324
150
  # Sets the value on the instance
325
151
  #
326
- # @return [Object]
327
- # value of an attribute
152
+ # @example
153
+ # attribute.set(instance, value) # => value
328
154
  #
329
- # @api private
155
+ # @return [self]
156
+ #
157
+ # @api public
330
158
  def set(instance, value)
331
- set!(instance, typecast(value)) unless value.nil?
159
+ set!(instance, coerce(value))
332
160
  end
333
161
 
334
162
  # Sets instance variable of the attribute
335
163
  #
336
- # @return [Object]
337
- # value of an attribute
164
+ # @example
165
+ # attribute.set!(instance, value) # => value
338
166
  #
339
- # @api private
167
+ # @return [self]
168
+ #
169
+ # @api public
340
170
  def set!(instance, value)
341
171
  instance.instance_variable_set(instance_variable_name, value)
342
172
  end
343
173
 
174
+ # Converts the given value to the primitive type
175
+ #
176
+ # @example
177
+ # attribute.coerce(value) # => primitive_value
178
+ #
179
+ # @param [Object] value
180
+ # the value
181
+ #
182
+ # @return [Object]
183
+ # nil, original value or value converted to the primitive type
184
+ #
185
+ # @api public
186
+ def coerce(value)
187
+ Coercion[value.class].send(coercion_method, value)
188
+ end
189
+
344
190
  # Creates an attribute reader method
345
191
  #
192
+ # @param [Module] mod
193
+ #
346
194
  # @return [self]
347
195
  #
348
196
  # @api private
349
- def add_reader_method(model)
350
- instance_variable_name = self.instance_variable_name
351
- method_name = name
352
-
353
- model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
354
- module AttributeMethods # module AttributeMethods
355
- def #{method_name} # def name
356
- return #{instance_variable_name} if defined?(#{instance_variable_name}) # return @name if defined?(@name)
357
- attribute = self.class.attributes[#{method_name.inspect}] # attribute = self.class.attributes[:name]
358
- #{instance_variable_name} = attribute ? attribute.get(self) : nil # @name = attribute ? attribute.get(self) : nil
359
- end # end
360
- end # end
361
- include AttributeMethods # include AttributeMethods
362
- RUBY
363
-
364
- model.send(reader_visibility, method_name)
197
+ def define_reader_method(mod)
198
+ reader_method_name = name
199
+ attribute = self
200
+
201
+ mod.send(:define_method, reader_method_name) { attribute.get(self) }
202
+ mod.send(reader_visibility, reader_method_name)
365
203
 
366
204
  self
367
205
  end
368
206
 
369
207
  # Creates an attribute writer method
370
208
  #
209
+ # @param [Module] mod
210
+ #
371
211
  # @return [self]
372
212
  #
373
213
  # @api private
374
- def add_writer_method(model)
375
- name = self.name
376
- method_name = "#{name}="
377
-
378
- model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
379
- module AttributeMethods # module AttributeMethods
380
- def #{method_name}(value) # def name=(value)
381
- self.class.attributes[#{name.inspect}].set(self, value) # self.class.attributes[:name].set(self, value)
382
- end # end
383
- end # end
384
- include AttributeMethods # include AttributeMethods
385
- RUBY
214
+ def define_writer_method(mod)
215
+ writer_method_name = "#{name}="
216
+ attribute = self
386
217
 
387
- model.send(writer_visibility, method_name)
218
+ mod.send(:define_method, writer_method_name) { |value| attribute.set(self, value) }
219
+ mod.send(writer_visibility, writer_method_name)
388
220
 
389
221
  self
390
222
  end
391
223
 
392
224
  private
393
225
 
226
+ # Sets a default value
227
+ #
228
+ # @param [Object]
229
+ #
230
+ # @return [Object]
231
+ # default value that was set
232
+ #
233
+ # @api private
234
+ def set_default_value(instance)
235
+ set!(instance, default.kind_of?(Proc) ? default.call(instance, self) : default)
236
+ end
237
+
394
238
  # Sets visibility of reader/write methods based on the options hash
395
239
  #
396
240
  # @return [undefined]
@@ -398,8 +242,24 @@ module Virtus
398
242
  # @api private
399
243
  def set_visibility
400
244
  default_accessor = @options.fetch(:accessor, self.class::DEFAULT_ACCESSOR)
401
- @reader_visibility = @options.fetch(:reader, default_accessor)
402
- @writer_visibility = @options.fetch(:writer, default_accessor)
245
+ @reader_visibility = @options.fetch(:reader, default_accessor)
246
+ @writer_visibility = @options.fetch(:writer, default_accessor)
247
+ end
248
+
249
+ # Sets default value option
250
+ #
251
+ # @return [Object]
252
+ #
253
+ # @api private
254
+ def set_default
255
+ default = @options[:default]
256
+
257
+ @default = case default
258
+ when Proc, ::NilClass, ::TrueClass, ::FalseClass, ::Numeric, ::Symbol
259
+ default
260
+ else
261
+ default.dup
262
+ end
403
263
  end
404
264
 
405
265
  end # class Attribute