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.
- data/.travis.yml +9 -2
- data/.yardopts +1 -0
- data/History.md +51 -0
- data/{README.markdown → README.md} +63 -7
- data/TODO +2 -4
- data/VERSION +1 -1
- data/config/flay.yml +2 -2
- data/config/flog.yml +1 -1
- data/config/roodi.yml +5 -6
- data/config/site.reek +3 -3
- data/examples/custom_coercion_spec.rb +50 -0
- data/examples/default_values_spec.rb +21 -0
- data/lib/virtus.rb +21 -6
- data/lib/virtus/attribute.rb +113 -253
- data/lib/virtus/attribute/array.rb +6 -3
- data/lib/virtus/attribute/boolean.rb +9 -28
- data/lib/virtus/attribute/date.rb +9 -12
- data/lib/virtus/attribute/date_time.rb +10 -12
- data/lib/virtus/attribute/decimal.rb +4 -11
- data/lib/virtus/attribute/float.rb +4 -11
- data/lib/virtus/attribute/hash.rb +5 -3
- data/lib/virtus/attribute/integer.rb +4 -11
- data/lib/virtus/attribute/numeric.rb +1 -0
- data/lib/virtus/attribute/object.rb +1 -0
- data/lib/virtus/attribute/string.rb +4 -11
- data/lib/virtus/attribute/time.rb +9 -16
- data/lib/virtus/class_methods.rb +42 -7
- data/lib/virtus/coercion.rb +32 -0
- data/lib/virtus/coercion/date.rb +26 -0
- data/lib/virtus/coercion/date_time.rb +26 -0
- data/lib/virtus/coercion/decimal.rb +40 -0
- data/lib/virtus/coercion/false_class.rb +24 -0
- data/lib/virtus/coercion/float.rb +24 -0
- data/lib/virtus/coercion/hash.rb +82 -0
- data/lib/virtus/coercion/integer.rb +60 -0
- data/lib/virtus/coercion/numeric.rb +66 -0
- data/lib/virtus/coercion/object.rb +25 -0
- data/lib/virtus/coercion/string.rb +155 -0
- data/lib/virtus/coercion/symbol.rb +24 -0
- data/lib/virtus/coercion/time.rb +26 -0
- data/lib/virtus/coercion/time_coercions.rb +85 -0
- data/lib/virtus/coercion/true_class.rb +24 -0
- data/lib/virtus/instance_methods.rb +7 -0
- data/lib/virtus/support/descendants_tracker.rb +1 -1
- data/lib/virtus/support/options.rb +114 -0
- data/lib/virtus/support/type_lookup.rb +95 -0
- data/spec/integration/virtus/attributes/attribute/{typecast_spec.rb → set_spec.rb} +7 -7
- data/spec/unit/shared/attribute.rb +3 -3
- data/spec/unit/shared/attribute/accept_options.rb +0 -18
- data/spec/unit/shared/attribute/accepted_options.rb +0 -6
- data/spec/unit/shared/attribute/get.rb +32 -17
- data/spec/unit/shared/attribute/inspect.rb +7 -0
- data/spec/unit/shared/attribute/primitive.rb +15 -0
- data/spec/unit/shared/attribute/set.rb +16 -21
- data/spec/unit/virtus/attribute/array_spec.rb +18 -3
- data/spec/unit/virtus/attribute/boolean_spec.rb +8 -6
- data/spec/unit/virtus/attribute/date_spec.rb +8 -6
- data/spec/unit/virtus/attribute/date_time_spec.rb +8 -6
- data/spec/unit/virtus/attribute/decimal_spec.rb +18 -6
- data/spec/unit/virtus/attribute/float_spec.rb +19 -7
- data/spec/unit/virtus/attribute/hash_spec.rb +5 -3
- data/spec/unit/virtus/attribute/integer_spec.rb +10 -8
- data/spec/unit/virtus/attribute/string_spec.rb +10 -8
- data/spec/unit/virtus/attribute/time_spec.rb +8 -6
- data/spec/unit/virtus/class_methods/attributes_spec.rb +11 -0
- data/spec/unit/virtus/coercion/class_name_reference_spec.rb +17 -0
- data/spec/unit/virtus/coercion/date/class_methods/to_datetime_spec.rb +30 -0
- data/spec/unit/virtus/coercion/date/class_methods/to_string_spec.rb +12 -0
- data/spec/unit/virtus/coercion/date/class_methods/to_time_spec.rb +12 -0
- data/spec/unit/virtus/coercion/date_time/class_methods/to_date_spec.rb +30 -0
- data/spec/unit/virtus/coercion/date_time/class_methods/to_string_spec.rb +12 -0
- data/spec/unit/virtus/coercion/date_time/class_methods/to_time_spec.rb +30 -0
- data/spec/unit/virtus/coercion/decimal/class_methods/to_float_spec.rb +12 -0
- data/spec/unit/virtus/coercion/decimal/class_methods/to_integer_spec.rb +12 -0
- data/spec/unit/virtus/coercion/decimal/class_methods/to_string_spec.rb +12 -0
- data/spec/unit/virtus/coercion/false_class/class_methods/to_string_spec.rb +12 -0
- data/spec/unit/virtus/coercion/float/class_methods/to_decimal_spec.rb +12 -0
- data/spec/unit/virtus/coercion/float/class_methods/to_integer_spec.rb +12 -0
- data/spec/unit/virtus/coercion/float/class_methods/to_string_spec.rb +12 -0
- data/spec/unit/virtus/coercion/hash/class_methods/to_array_spec.rb +12 -0
- data/spec/unit/virtus/coercion/hash/class_methods/to_date_spec.rb +31 -0
- data/spec/unit/virtus/coercion/hash/class_methods/to_datetime_spec.rb +31 -0
- data/spec/unit/virtus/coercion/hash/class_methods/to_time_spec.rb +31 -0
- data/spec/unit/virtus/coercion/integer/class_methods/to_boolean_spec.rb +25 -0
- data/spec/unit/virtus/coercion/integer/class_methods/to_decimal_spec.rb +12 -0
- data/spec/unit/virtus/coercion/integer/class_methods/to_float_spec.rb +12 -0
- data/spec/unit/virtus/coercion/integer/class_methods/to_string_spec.rb +12 -0
- data/spec/unit/virtus/coercion/object/class_methods/method_missing_spec.rb +33 -0
- data/spec/unit/virtus/coercion/string/class_methods/to_boolean_spec.rb +29 -0
- data/spec/unit/virtus/coercion/string/class_methods/to_date_spec.rb +23 -0
- data/spec/unit/virtus/coercion/string/class_methods/to_datetime_spec.rb +50 -0
- data/spec/unit/virtus/coercion/string/class_methods/to_decimal_spec.rb +23 -0
- data/spec/unit/virtus/coercion/string/class_methods/to_float_spec.rb +21 -0
- data/spec/unit/virtus/coercion/string/class_methods/to_integer_spec.rb +21 -0
- data/spec/unit/virtus/coercion/string/class_methods/to_time_spec.rb +50 -0
- data/spec/unit/virtus/coercion/symbol/class_methods/to_string_spec.rb +12 -0
- data/spec/unit/virtus/coercion/true_class/class_methods/to_string_spec.rb +12 -0
- data/spec/unit/virtus/instance_methods/attributes_spec.rb +7 -0
- data/spec/unit/virtus/options/accept_options_spec.rb +38 -0
- data/spec/unit/virtus/options/accepted_options_spec.rb +21 -0
- data/spec/unit/virtus/options/options_spec.rb +11 -0
- data/spec/unit/virtus/type_lookup/determine_type_spec.rb +68 -0
- data/spec/unit/virtus/type_lookup/primitive_spec.rb +9 -0
- data/virtus.gemspec +70 -17
- metadata +78 -27
- data/History.txt +0 -38
- data/lib/virtus/typecast/boolean.rb +0 -29
- data/lib/virtus/typecast/numeric.rb +0 -87
- data/lib/virtus/typecast/string.rb +0 -24
- data/lib/virtus/typecast/time.rb +0 -192
- data/spec/unit/shared/attribute/complex.rb +0 -15
- data/spec/unit/shared/attribute/options.rb +0 -7
- data/spec/unit/virtus/attribute/attribute_spec.rb +0 -12
data/.travis.yml
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
|
|
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.
|
|
6
|
-
attributes on a model
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
2
|
-
* Add
|
|
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.
|
|
1
|
+
0.0.6
|
data/config/flay.yml
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
---
|
|
2
|
-
threshold:
|
|
3
|
-
total_score:
|
|
2
|
+
threshold: 20
|
|
3
|
+
total_score: 268
|
data/config/flog.yml
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
---
|
|
2
|
-
threshold:
|
|
2
|
+
threshold: 15.9
|
data/config/roodi.yml
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
---
|
|
2
|
-
AbcMetricMethodCheck: { score:
|
|
2
|
+
AbcMetricMethodCheck: { score: 8.1 }
|
|
3
3
|
AssignmentInConditionalCheck: { }
|
|
4
4
|
CaseMissingElseCheck: { }
|
|
5
|
-
ClassLineCountCheck: { line_count:
|
|
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:
|
|
9
|
+
CyclomaticComplexityMethodCheck: { complexity: 4 }
|
|
10
10
|
EmptyRescueBodyCheck: { }
|
|
11
11
|
ForLoopCheck: { }
|
|
12
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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 [
|
|
16
|
+
# @return [undefined]
|
|
17
17
|
#
|
|
18
18
|
# @api private
|
|
19
19
|
def self.included(descendant)
|
|
20
|
-
|
|
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/
|
|
35
|
-
require 'virtus/
|
|
36
|
-
require 'virtus/
|
|
37
|
-
require 'virtus/
|
|
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'
|
data/lib/virtus/attribute.rb
CHANGED
|
@@ -5,185 +5,8 @@ module Virtus
|
|
|
5
5
|
# @abstract
|
|
6
6
|
class Attribute
|
|
7
7
|
extend DescendantsTracker
|
|
8
|
-
|
|
9
|
-
|
|
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, :
|
|
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
|
-
@
|
|
98
|
+
@coercion_method = @options.fetch(:coercion_method)
|
|
261
99
|
|
|
100
|
+
set_default
|
|
262
101
|
set_visibility
|
|
263
102
|
end
|
|
264
103
|
|
|
265
|
-
# Returns
|
|
104
|
+
# Returns a concise string representation of the attribute instance
|
|
266
105
|
#
|
|
267
106
|
# @example
|
|
268
|
-
# Virtus::Attribute::String.
|
|
269
|
-
# Virtus::Attribute::
|
|
107
|
+
# attribute = Virtus::Attribute::String.new(:name)
|
|
108
|
+
# attribute.inspect # => #<Virtus::Attribute::String @name=:name>
|
|
270
109
|
#
|
|
271
|
-
# @return [
|
|
110
|
+
# @return [String]
|
|
272
111
|
#
|
|
273
112
|
# @api public
|
|
274
|
-
def
|
|
275
|
-
@
|
|
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
|
-
#
|
|
117
|
+
# Returns value of an attribute for the given instance
|
|
296
118
|
#
|
|
297
|
-
#
|
|
119
|
+
# Sets the default value if an ivar is not set and default
|
|
120
|
+
# value is configured
|
|
298
121
|
#
|
|
299
|
-
# @
|
|
300
|
-
|
|
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
|
|
128
|
+
# @api public
|
|
310
129
|
def get(instance)
|
|
311
|
-
|
|
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
|
|
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
|
-
# @
|
|
327
|
-
# value
|
|
152
|
+
# @example
|
|
153
|
+
# attribute.set(instance, value) # => value
|
|
328
154
|
#
|
|
329
|
-
# @
|
|
155
|
+
# @return [self]
|
|
156
|
+
#
|
|
157
|
+
# @api public
|
|
330
158
|
def set(instance, value)
|
|
331
|
-
set!(instance,
|
|
159
|
+
set!(instance, coerce(value))
|
|
332
160
|
end
|
|
333
161
|
|
|
334
162
|
# Sets instance variable of the attribute
|
|
335
163
|
#
|
|
336
|
-
# @
|
|
337
|
-
# value
|
|
164
|
+
# @example
|
|
165
|
+
# attribute.set!(instance, value) # => value
|
|
338
166
|
#
|
|
339
|
-
# @
|
|
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
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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,
|
|
402
|
-
@writer_visibility = @options.fetch(:writer,
|
|
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
|