virtus 0.0.5 → 0.0.6

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