attr_typecastable 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.travis.yml +8 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +137 -0
- data/Rakefile +7 -0
- data/attr_typecastable.gemspec +26 -0
- data/lib/attr_typecastable.rb +76 -0
- data/lib/attr_typecastable/initialize_hook.rb +17 -0
- data/lib/attr_typecastable/reflection.rb +20 -0
- data/lib/attr_typecastable/types.rb +48 -0
- data/lib/attr_typecastable/types/array.rb +28 -0
- data/lib/attr_typecastable/types/base.rb +24 -0
- data/lib/attr_typecastable/types/big_decimal.rb +31 -0
- data/lib/attr_typecastable/types/boolean.rb +28 -0
- data/lib/attr_typecastable/types/date.rb +21 -0
- data/lib/attr_typecastable/types/date_time.rb +24 -0
- data/lib/attr_typecastable/types/exception.rb +9 -0
- data/lib/attr_typecastable/types/float.rb +18 -0
- data/lib/attr_typecastable/types/integer.rb +18 -0
- data/lib/attr_typecastable/types/object.rb +14 -0
- data/lib/attr_typecastable/types/rational.rb +18 -0
- data/lib/attr_typecastable/types/string.rb +18 -0
- data/lib/attr_typecastable/types/symbol.rb +18 -0
- data/lib/attr_typecastable/types/time.rb +20 -0
- data/lib/attr_typecastable/version.rb +3 -0
- data/spec/attr_typecastable_spec.rb +226 -0
- data/spec/performance_check.rb +14 -0
- data/spec/spec_helper.rb +9 -0
- metadata +147 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d6d4f8c8175714df9ce86adf7985697d25c937a7
|
4
|
+
data.tar.gz: 30969bfe24b9062d8b0d74db2faa4a8fb7c82b20
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1e206d63240dc70bdb5241706a31c03bd2da6ad69db558ae4f07ede3ceae278ac5568fab6d39280b521ffd6143ff24a5a25ab6c3e57a9733a28f95b7d6a7a3af
|
7
|
+
data.tar.gz: a9f4453895c2fb5f5af9f0f76cccb5408731749b0550b2c8a5daaa59ea32b11e037ddf61ef1879978014919830d8495d2ad3bb39f4d221c5266d39412ac47b89
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in attr_typecastable.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem 'rspec-power_assert', github: 'joker1007/rspec-power_assert'
|
7
|
+
gem 'power_assert', github: 'k-tsj/power_assert'
|
8
|
+
gem 'benchmark-ips'
|
9
|
+
gem 'seeing_is_believing'
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 joker1007
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
# attr\_typecastable
|
2
|
+
[![Build Status](https://travis-ci.org/joker1007/attr_typecastable.svg?branch=master)](https://travis-ci.org/joker1007/attr_typecastable)
|
3
|
+
[![Coverage Status](https://coveralls.io/repos/joker1007/attr_typecastable/badge.png)](https://coveralls.io/r/joker1007/attr_typecastable)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/joker1007/attr_typecastable/badges/gpa.svg)](https://codeclimate.com/github/joker1007/attr_typecastable)
|
5
|
+
|
6
|
+
attr\_accessor with typecast feature.
|
7
|
+
|
8
|
+
## Requirements
|
9
|
+
- Ruby 2.0 or later
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'attr_typecastable'
|
17
|
+
```
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install attr_typecastable
|
26
|
+
|
27
|
+
## Default Typecasters
|
28
|
+
- Object (no typecasting)
|
29
|
+
- String
|
30
|
+
- Integer
|
31
|
+
- Float
|
32
|
+
- Rational
|
33
|
+
- BigDecimal
|
34
|
+
- Boolean
|
35
|
+
- Time
|
36
|
+
- Date
|
37
|
+
- DateTime
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
require 'attr_typecastable'
|
43
|
+
|
44
|
+
# Custome typecaster definition
|
45
|
+
class CastToMoney < AttrTypecastable::Types::Base
|
46
|
+
private
|
47
|
+
|
48
|
+
# must define #do_typecast method
|
49
|
+
def do_typecast(value)
|
50
|
+
# can use @options
|
51
|
+
# @options is hash given to `typed_attr_accessor`
|
52
|
+
return value if value.is_a?(Money)
|
53
|
+
|
54
|
+
if value.is_a?(Integer)
|
55
|
+
Money.new(value)
|
56
|
+
else
|
57
|
+
raise AttrTypecastable::Types::CastError, "Cannot convert to Money"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class User
|
63
|
+
# define typed_attr_accessor and initialize hook
|
64
|
+
include AttrTypecastable
|
65
|
+
|
66
|
+
typed_attr_accessor :name, String
|
67
|
+
|
68
|
+
# String typecaster uses #to_s
|
69
|
+
typed_attr_accessor :default_name, String, default: "Foo"
|
70
|
+
typed_attr_accessor :not_nil_name, String, allow_nil: false, default: ""
|
71
|
+
|
72
|
+
# Date typecaster uses #to_date
|
73
|
+
typed_attr_accessor :birthday, Date
|
74
|
+
|
75
|
+
# Time typecaster uses #to_time
|
76
|
+
typed_attr_accessor :birthday_time, Time
|
77
|
+
|
78
|
+
# DateTime typecaster uses #to_time and #to_datetime
|
79
|
+
typed_attr_accessor :birthday_datetime, DateTime
|
80
|
+
|
81
|
+
# Integer typecaster uses #to_i
|
82
|
+
typed_attr_accessor :age, Integer
|
83
|
+
|
84
|
+
# Float typecaster uses #to_f
|
85
|
+
typed_attr_accessor :bmi, Float
|
86
|
+
|
87
|
+
# Custom Typecaster
|
88
|
+
typed_attr_accessor :property, CastToMoney
|
89
|
+
|
90
|
+
# Boolean typecaster uses `#===` method in order to check whether value is true or false.
|
91
|
+
typed_attr_accessor :adult, Boolean # default true_values: ["true", 1], false_values: ["false", 0]
|
92
|
+
typed_attr_accessor :admin, Boolean, true_value: ["yes"]
|
93
|
+
typed_attr_accessor :active, Boolean, true_value: [/true/i] # can use Regexp
|
94
|
+
|
95
|
+
def initialize(name: nil, default_name: nil)
|
96
|
+
self.name = name
|
97
|
+
self.default_name = default_name
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
user = User.new
|
102
|
+
user.name # => nil
|
103
|
+
user.default_name # => "Foo"
|
104
|
+
|
105
|
+
user.name = 1
|
106
|
+
user.name # => "1"
|
107
|
+
|
108
|
+
user.birthday = "1990-10-1"
|
109
|
+
user.birthday # => Mon, 01 Oct 1990
|
110
|
+
|
111
|
+
user.adult = "true"
|
112
|
+
user.adult # => true
|
113
|
+
|
114
|
+
user.adult = "false"
|
115
|
+
user.adult # => false
|
116
|
+
|
117
|
+
user.adult = "other"
|
118
|
+
user.adult # => true
|
119
|
+
|
120
|
+
user.admin = "yes"
|
121
|
+
user.admin # => true
|
122
|
+
|
123
|
+
user.active = "TrUe"
|
124
|
+
user.active # => true
|
125
|
+
|
126
|
+
user2 = User.new(default_name: "joker")
|
127
|
+
user2.default_name # => "joker"
|
128
|
+
```
|
129
|
+
|
130
|
+
|
131
|
+
## Contributing
|
132
|
+
|
133
|
+
1. Fork it ( https://github.com/joker1007/attr_typecastable/fork )
|
134
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
135
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
136
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
137
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'attr_typecastable/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "attr_typecastable"
|
8
|
+
spec.version = AttrTypecastable::VERSION
|
9
|
+
spec.authors = ["joker1007"]
|
10
|
+
spec.email = ["kakyoin.hierophant@gmail.com"]
|
11
|
+
spec.summary = %q{attr_accessor with typecast feature.}
|
12
|
+
spec.description = %q{attr_accessor with typecast feature.}
|
13
|
+
spec.homepage = "https://github.com/joker1007/attr_typecastable"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency "activesupport", ">= 3.2"
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "rspec"
|
25
|
+
spec.add_development_dependency "coveralls"
|
26
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require "attr_typecastable/version"
|
2
|
+
require "attr_typecastable/types"
|
3
|
+
require "attr_typecastable/initialize_hook"
|
4
|
+
require "attr_typecastable/reflection"
|
5
|
+
|
6
|
+
require "active_support/concern"
|
7
|
+
require "active_support/core_ext/class"
|
8
|
+
require "active_support/core_ext/hash"
|
9
|
+
|
10
|
+
module AttrTypecastable
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
included do
|
14
|
+
class_attribute(:typed_attr_reflections)
|
15
|
+
self.typed_attr_reflections = {}
|
16
|
+
prepend InitializeHook
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
DEFAULT_OPTIONS = {
|
21
|
+
allow_nil: true,
|
22
|
+
cast_method: true,
|
23
|
+
reset_method: true,
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
def typed_attr_accessor(attribute_name, typecaster_name = Object, **options)
|
27
|
+
attribute_name = attribute_name.to_sym
|
28
|
+
options = DEFAULT_OPTIONS.merge(options)
|
29
|
+
must_have_default_when_disallow_nil(options)
|
30
|
+
|
31
|
+
typecaster = Types.typecaster_for(typecaster_name || Object).new(options)
|
32
|
+
typed_attr_reflections[attribute_name] = Reflection.new(attribute_name, typecaster, options)
|
33
|
+
|
34
|
+
attr_reader attribute_name
|
35
|
+
|
36
|
+
define_typed_attr_writer(attribute_name)
|
37
|
+
define_cast_attribute(attribute_name) if options[:cast_method]
|
38
|
+
define_reset_attribute(attribute_name) if options[:reset_method]
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def define_typed_attr_writer(attribute_name)
|
44
|
+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
45
|
+
def #{attribute_name}=(value)
|
46
|
+
typecaster = self.class.typed_attr_reflections[:#{attribute_name}].typecaster
|
47
|
+
@#{attribute_name} = typecaster.typecast(value)
|
48
|
+
end
|
49
|
+
RUBY
|
50
|
+
end
|
51
|
+
|
52
|
+
def define_cast_attribute(attribute_name)
|
53
|
+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
54
|
+
def cast_#{attribute_name}!
|
55
|
+
value = @#{attribute_name}
|
56
|
+
self.#{attribute_name} = value
|
57
|
+
end
|
58
|
+
RUBY
|
59
|
+
end
|
60
|
+
|
61
|
+
def define_reset_attribute(attribute_name, **options)
|
62
|
+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
63
|
+
def reset_#{attribute_name}!
|
64
|
+
default = self.class.typed_attr_reflections[:#{attribute_name}].default
|
65
|
+
value = default.respond_to?(:call) ? default.call(self, :#{attribute_name}) : default
|
66
|
+
self.#{attribute_name} = value
|
67
|
+
end
|
68
|
+
RUBY
|
69
|
+
end
|
70
|
+
|
71
|
+
def must_have_default_when_disallow_nil(options)
|
72
|
+
raise "Need `default` option when allow_nil is false" \
|
73
|
+
if options[:allow_nil].! && options[:default].nil?
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module AttrTypecastable
|
2
|
+
module InitializeHook
|
3
|
+
def initialize(*)
|
4
|
+
super
|
5
|
+
|
6
|
+
self.class.typed_attr_reflections.select {|_, r| r.has_default?}.each do |attr, reflection|
|
7
|
+
current = send("#{attr}")
|
8
|
+
next unless current.nil?
|
9
|
+
|
10
|
+
default = reflection.default
|
11
|
+
|
12
|
+
v = default.respond_to?(:call) ? default.call(self, attr) : default
|
13
|
+
send("#{attr}=", v)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module AttrTypecastable
|
2
|
+
class Reflection
|
3
|
+
attr_reader :attribute_name, :typecaster, :options
|
4
|
+
|
5
|
+
def initialize(attribute_name, typecaster, **options)
|
6
|
+
@attribute_name = attribute_name
|
7
|
+
@typecaster = typecaster
|
8
|
+
@options = options
|
9
|
+
freeze
|
10
|
+
end
|
11
|
+
|
12
|
+
def default
|
13
|
+
@options[:default]
|
14
|
+
end
|
15
|
+
|
16
|
+
def has_default?
|
17
|
+
@options.key?(:default)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'bigdecimal'
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
require 'attr_typecastable/types/exception'
|
6
|
+
require 'attr_typecastable/types/string'
|
7
|
+
require 'attr_typecastable/types/symbol'
|
8
|
+
require 'attr_typecastable/types/integer'
|
9
|
+
require 'attr_typecastable/types/time'
|
10
|
+
require 'attr_typecastable/types/date'
|
11
|
+
require 'attr_typecastable/types/date_time'
|
12
|
+
require 'attr_typecastable/types/float'
|
13
|
+
require 'attr_typecastable/types/rational'
|
14
|
+
require 'attr_typecastable/types/big_decimal'
|
15
|
+
require 'attr_typecastable/types/boolean'
|
16
|
+
require 'attr_typecastable/types/object'
|
17
|
+
require 'attr_typecastable/types/array'
|
18
|
+
|
19
|
+
unless defined?(::Boolean)
|
20
|
+
class Boolean; end
|
21
|
+
end
|
22
|
+
|
23
|
+
module AttrTypecastable
|
24
|
+
module Types
|
25
|
+
BASIC_TYPES = {
|
26
|
+
::Object => AttrTypecastable::Types::Object,
|
27
|
+
::String => AttrTypecastable::Types::String,
|
28
|
+
::Symbol => AttrTypecastable::Types::Symbol,
|
29
|
+
::Integer => AttrTypecastable::Types::Integer,
|
30
|
+
::Time => AttrTypecastable::Types::Time,
|
31
|
+
::Date => AttrTypecastable::Types::Date,
|
32
|
+
::DateTime => AttrTypecastable::Types::DateTime,
|
33
|
+
::Float => AttrTypecastable::Types::Float,
|
34
|
+
::Rational => AttrTypecastable::Types::Rational,
|
35
|
+
::BigDecimal => AttrTypecastable::Types::BigDecimal,
|
36
|
+
::Boolean => AttrTypecastable::Types::Boolean,
|
37
|
+
}.freeze
|
38
|
+
|
39
|
+
class << self
|
40
|
+
def typecaster_for(typecaster, **options)
|
41
|
+
const = typecaster.is_a?(::String) ? typecaster.constantize : typecaster
|
42
|
+
BASIC_TYPES.fetch(const, const)
|
43
|
+
rescue NameError
|
44
|
+
raise TypeCasterNotFound, "#{typecaster} is undefined"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'attr_typecastable/types/exception'
|
2
|
+
require 'attr_typecastable/types/base'
|
3
|
+
|
4
|
+
require "active_support/core_ext/class"
|
5
|
+
|
6
|
+
module AttrTypecastable
|
7
|
+
module Types
|
8
|
+
class ArrayBase < Base
|
9
|
+
class_attribute :inner_typecaster
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def do_typecast(value)
|
14
|
+
wrapped = Array(value)
|
15
|
+
wrapped.map { |v| inner_typecaster.typecast(v) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class ArrayFactory
|
20
|
+
def self.build(typecaster_name, **options)
|
21
|
+
typecaster = Types.typecaster_for(typecaster_name).new(options)
|
22
|
+
klass = Class.new(ArrayBase)
|
23
|
+
klass.inner_typecaster = typecaster
|
24
|
+
klass
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module AttrTypecastable
|
2
|
+
module Types
|
3
|
+
class Base
|
4
|
+
def initialize(**options)
|
5
|
+
@options = options
|
6
|
+
end
|
7
|
+
|
8
|
+
def typecast(value)
|
9
|
+
return nil if allow_nil_and_nil_value?(value)
|
10
|
+
do_typecast(value)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def do_typecast(value)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def allow_nil_and_nil_value?(value)
|
20
|
+
@options[:allow_nil] && value.nil?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'attr_typecastable/types/exception'
|
2
|
+
require 'attr_typecastable/types/base'
|
3
|
+
module AttrTypecastable
|
4
|
+
module Types
|
5
|
+
class BigDecimal < Base
|
6
|
+
def do_typecast(value)
|
7
|
+
require 'bigdecimal/util'
|
8
|
+
|
9
|
+
return value if value.is_a?(::BigDecimal)
|
10
|
+
|
11
|
+
if value.is_a?(::Rational)
|
12
|
+
if @options[:fig]
|
13
|
+
value.to_d(@options[:fig])
|
14
|
+
else
|
15
|
+
value.to_f.to_d
|
16
|
+
end
|
17
|
+
elsif value.is_a?(::Float)
|
18
|
+
if @options[:precision]
|
19
|
+
value.to_d(@options[:precision])
|
20
|
+
else
|
21
|
+
value.to_d
|
22
|
+
end
|
23
|
+
elsif value.respond_to?(:to_d)
|
24
|
+
value.to_d
|
25
|
+
else
|
26
|
+
BigDecimal.new(value.to_s)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'attr_typecastable/types/exception'
|
2
|
+
require 'attr_typecastable/types/base'
|
3
|
+
|
4
|
+
module AttrTypecastable
|
5
|
+
module Types
|
6
|
+
class Boolean < Base
|
7
|
+
def initialize(**options)
|
8
|
+
super
|
9
|
+
@options[:true_values] ||= ["true", 1]
|
10
|
+
@options[:false_values] ||= ["false", 0]
|
11
|
+
end
|
12
|
+
|
13
|
+
def do_typecast(value)
|
14
|
+
return value if value.is_a?(::TrueClass) || value.is_a?(::FalseClass)
|
15
|
+
|
16
|
+
true_values = Array(@options[:true_values])
|
17
|
+
false_values = Array(@options[:false_values])
|
18
|
+
if true_values.any? {|v| v === value }
|
19
|
+
true
|
20
|
+
elsif false_values.any? {|v| v === value }
|
21
|
+
false
|
22
|
+
else
|
23
|
+
!!value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'attr_typecastable/types/exception'
|
2
|
+
require 'attr_typecastable/types/base'
|
3
|
+
|
4
|
+
require 'date'
|
5
|
+
require 'active_support/core_ext/string'
|
6
|
+
|
7
|
+
module AttrTypecastable
|
8
|
+
module Types
|
9
|
+
class Date < Base
|
10
|
+
def do_typecast(value)
|
11
|
+
return value if value.is_a?(::Date)
|
12
|
+
|
13
|
+
if value.respond_to?(:to_date)
|
14
|
+
value.to_date
|
15
|
+
else
|
16
|
+
raise CastError, "value does not have `to_date` method"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'attr_typecastable/types/exception'
|
2
|
+
require 'attr_typecastable/types/base'
|
3
|
+
|
4
|
+
require 'date'
|
5
|
+
require 'active_support/core_ext/string'
|
6
|
+
|
7
|
+
module AttrTypecastable
|
8
|
+
module Types
|
9
|
+
class DateTime < Base
|
10
|
+
def do_typecast(value)
|
11
|
+
return value if value.is_a?(::DateTime)
|
12
|
+
|
13
|
+
case value
|
14
|
+
when ::String
|
15
|
+
value.to_time.to_datetime
|
16
|
+
when ::Time, ::Date
|
17
|
+
value.to_datetime
|
18
|
+
else
|
19
|
+
raise CastError, "cannot convert to DateTime"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'attr_typecastable/types/exception'
|
2
|
+
require 'attr_typecastable/types/base'
|
3
|
+
|
4
|
+
module AttrTypecastable
|
5
|
+
module Types
|
6
|
+
class Float < Base
|
7
|
+
def do_typecast(value)
|
8
|
+
return value if value.is_a?(::Float)
|
9
|
+
|
10
|
+
if value.respond_to?(:to_f)
|
11
|
+
value.to_f
|
12
|
+
else
|
13
|
+
raise CastError, "value does not have `to_f` method"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'attr_typecastable/types/exception'
|
2
|
+
require 'attr_typecastable/types/base'
|
3
|
+
|
4
|
+
module AttrTypecastable
|
5
|
+
module Types
|
6
|
+
class Integer < Base
|
7
|
+
def do_typecast(value)
|
8
|
+
return value if value.is_a?(::Integer)
|
9
|
+
|
10
|
+
if value.respond_to?(:to_i)
|
11
|
+
value.to_i
|
12
|
+
else
|
13
|
+
raise CastError, "value does not have `to_i` method"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'attr_typecastable/types/exception'
|
2
|
+
require 'attr_typecastable/types/base'
|
3
|
+
|
4
|
+
module AttrTypecastable
|
5
|
+
module Types
|
6
|
+
class Rational < Base
|
7
|
+
def do_typecast(value)
|
8
|
+
return value if value.is_a?(::Rational)
|
9
|
+
|
10
|
+
if value.respond_to?(:to_r)
|
11
|
+
value.to_r
|
12
|
+
else
|
13
|
+
raise CastError, "value does not have `to_r` method"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'attr_typecastable/types/exception'
|
2
|
+
require 'attr_typecastable/types/base'
|
3
|
+
|
4
|
+
module AttrTypecastable
|
5
|
+
module Types
|
6
|
+
class String < Base
|
7
|
+
def do_typecast(value)
|
8
|
+
return value if value.is_a?(::String)
|
9
|
+
|
10
|
+
if value.respond_to?(:to_s)
|
11
|
+
value.to_s
|
12
|
+
else
|
13
|
+
raise CastError, "value does not have `to_s` method"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'attr_typecastable/types/exception'
|
2
|
+
require 'attr_typecastable/types/base'
|
3
|
+
|
4
|
+
module AttrTypecastable
|
5
|
+
module Types
|
6
|
+
class Symbol < Base
|
7
|
+
def do_typecast(value)
|
8
|
+
return value if value.is_a?(::Symbol)
|
9
|
+
|
10
|
+
if value.respond_to?(:to_sym)
|
11
|
+
value.to_sym
|
12
|
+
else
|
13
|
+
raise CastError, "value does not have `to_sym` method"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'attr_typecastable/types/exception'
|
2
|
+
require 'attr_typecastable/types/base'
|
3
|
+
|
4
|
+
require 'active_support/core_ext/string'
|
5
|
+
|
6
|
+
module AttrTypecastable
|
7
|
+
module Types
|
8
|
+
class Time < Base
|
9
|
+
def do_typecast(value)
|
10
|
+
return value if value.is_a?(::Time)
|
11
|
+
|
12
|
+
if value.respond_to?(:to_time)
|
13
|
+
value.to_time
|
14
|
+
else
|
15
|
+
raise CastError, "value does not have `to_time` method"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AttrTypecastable do
|
4
|
+
class Money
|
5
|
+
include AttrTypecastable
|
6
|
+
typed_attr_accessor :value, Integer
|
7
|
+
|
8
|
+
def initialize(value)
|
9
|
+
self.value = value
|
10
|
+
end
|
11
|
+
|
12
|
+
def ==(other)
|
13
|
+
return false unless other.is_a?(Money)
|
14
|
+
|
15
|
+
@value == other.value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class CastToMoney < AttrTypecastable::Types::Base
|
20
|
+
private
|
21
|
+
|
22
|
+
def do_typecast(value)
|
23
|
+
return value if value.is_a?(Money)
|
24
|
+
|
25
|
+
if value.is_a?(Integer)
|
26
|
+
Money.new(value)
|
27
|
+
else
|
28
|
+
raise AttrTypecastable::Types::CastError, "Cannot convert to Money"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class User
|
34
|
+
include AttrTypecastable
|
35
|
+
|
36
|
+
typed_attr_accessor :name, String
|
37
|
+
typed_attr_accessor :default_name, String, default: "Foo"
|
38
|
+
typed_attr_accessor :conditional_default, String, default: proc { |user|
|
39
|
+
"Hello #{user.name}"
|
40
|
+
}
|
41
|
+
typed_attr_accessor :not_nil_name, String, allow_nil: false, default: ""
|
42
|
+
typed_attr_accessor :role, Symbol
|
43
|
+
typed_attr_accessor :birthday, "Date"
|
44
|
+
typed_attr_accessor :birthday_time, Time
|
45
|
+
typed_attr_accessor :birthday_datetime, DateTime
|
46
|
+
typed_attr_accessor :age, Integer
|
47
|
+
typed_attr_accessor :bmi, Float
|
48
|
+
typed_attr_accessor :bmi_r, Rational
|
49
|
+
typed_attr_accessor :property, CastToMoney
|
50
|
+
typed_attr_accessor :debt, BigDecimal
|
51
|
+
typed_attr_accessor :debt_with_precision, BigDecimal, precision: 3
|
52
|
+
typed_attr_accessor :debt_with_fig, BigDecimal, fig: 5
|
53
|
+
typed_attr_accessor :adult, Boolean
|
54
|
+
typed_attr_accessor :admin, Boolean, true_value: ["yes"]
|
55
|
+
typed_attr_accessor :active, Boolean, true_value: [/true/i]
|
56
|
+
typed_attr_accessor :manager, Boolean, allow_nil: false, default: false
|
57
|
+
typed_attr_accessor :raw_value, nil, default: 1
|
58
|
+
typed_attr_accessor :raw_value2, Object
|
59
|
+
typed_attr_accessor :raw_value3, default: "raw_value3"
|
60
|
+
|
61
|
+
typed_attr_accessor :skills, AttrTypecastable::Types::ArrayFactory.build(String), default: []
|
62
|
+
typed_attr_accessor :friend_ids, AttrTypecastable::Types::ArrayFactory.build(Integer)
|
63
|
+
typed_attr_accessor :nested_array,
|
64
|
+
AttrTypecastable::Types::ArrayFactory.build(
|
65
|
+
AttrTypecastable::Types::ArrayFactory.build(Integer)
|
66
|
+
)
|
67
|
+
|
68
|
+
def initialize(name: nil, default_name: nil)
|
69
|
+
self.name = name
|
70
|
+
self.default_name = default_name
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe ".typed_attr_accessor" do
|
75
|
+
it "defines attribute_accessor with typecasting" do
|
76
|
+
user = User.new
|
77
|
+
|
78
|
+
assert { user.default_name == "Foo" }
|
79
|
+
|
80
|
+
user.name = "joker"
|
81
|
+
assert { user.name == "joker" }
|
82
|
+
|
83
|
+
user.name = 10
|
84
|
+
assert { user.name == "10" }
|
85
|
+
|
86
|
+
user.name = nil
|
87
|
+
assert { user.name.nil? }
|
88
|
+
|
89
|
+
assert { user.not_nil_name == "" }
|
90
|
+
|
91
|
+
user.not_nil_name = nil
|
92
|
+
assert { user.not_nil_name == "" }
|
93
|
+
|
94
|
+
user.not_nil_name = "joker"
|
95
|
+
assert { user.not_nil_name == "joker" }
|
96
|
+
|
97
|
+
user.role = "super_user"
|
98
|
+
assert { user.role == :super_user }
|
99
|
+
|
100
|
+
user.age = 5
|
101
|
+
assert { user.age == 5 }
|
102
|
+
|
103
|
+
user.age = "unknown"
|
104
|
+
assert { user.age == 0 }
|
105
|
+
|
106
|
+
user.birthday = "2012-10-01"
|
107
|
+
assert { user.birthday == Date.new(2012, 10, 1) }
|
108
|
+
|
109
|
+
user.birthday = Date.new(2012, 10, 2)
|
110
|
+
assert { user.birthday == Date.new(2012, 10, 2) }
|
111
|
+
|
112
|
+
user.birthday_time = "2012-10-01 10:00:00"
|
113
|
+
assert { user.birthday_time == Time.new(2012, 10, 1, 10) }
|
114
|
+
|
115
|
+
user.birthday_datetime = "2012-10-01 10:00:00"
|
116
|
+
assert { user.birthday_datetime.is_a?(DateTime) }
|
117
|
+
assert { user.birthday_datetime == Time.new(2012, 10, 1, 10) }
|
118
|
+
|
119
|
+
user.birthday_datetime = Time.new(2012, 10, 2, 2)
|
120
|
+
assert { user.birthday_datetime.is_a?(DateTime) }
|
121
|
+
assert { user.birthday_datetime == Time.new(2012, 10, 2, 2) }
|
122
|
+
|
123
|
+
user.bmi = "20.1"
|
124
|
+
assert { user.bmi > 20.09 && user.bmi < 20.11 }
|
125
|
+
|
126
|
+
user.bmi = "20.1"
|
127
|
+
assert { user.bmi == "20.1".to_r }
|
128
|
+
|
129
|
+
user.property = 10000
|
130
|
+
assert { user.property == Money.new(10000) }
|
131
|
+
|
132
|
+
user.debt = 10000
|
133
|
+
assert { user.debt == BigDecimal.new("10000") }
|
134
|
+
user.debt = Rational(10000, 1)
|
135
|
+
assert { user.debt == BigDecimal.new("10000") }
|
136
|
+
user.debt = 10000.11
|
137
|
+
assert { user.debt == BigDecimal.new("10000.11") }
|
138
|
+
user.debt = "10000.12"
|
139
|
+
assert { user.debt == BigDecimal.new("10000.12") }
|
140
|
+
|
141
|
+
user.debt_with_precision = 1.3333333333
|
142
|
+
assert { user.debt_with_precision == BigDecimal.new("1.33") }
|
143
|
+
|
144
|
+
user.debt_with_fig = Rational("1000.11")
|
145
|
+
assert { user.debt_with_fig == BigDecimal.new("1000.1") }
|
146
|
+
|
147
|
+
user.adult = "true"
|
148
|
+
assert { user.adult == true }
|
149
|
+
|
150
|
+
user.adult = "other"
|
151
|
+
assert { user.adult == true }
|
152
|
+
|
153
|
+
user.adult = "false"
|
154
|
+
assert { user.adult == false }
|
155
|
+
|
156
|
+
user.adult = 1
|
157
|
+
assert { user.adult == true }
|
158
|
+
|
159
|
+
user.adult = 0
|
160
|
+
assert { user.adult == false }
|
161
|
+
|
162
|
+
user.admin = "yes"
|
163
|
+
assert { user.admin == true }
|
164
|
+
|
165
|
+
user.admin = false
|
166
|
+
assert { user.admin == false }
|
167
|
+
|
168
|
+
user.active = "tRUe"
|
169
|
+
assert { user.active == true }
|
170
|
+
|
171
|
+
assert { user.manager == false }
|
172
|
+
|
173
|
+
user.manager = "true"
|
174
|
+
assert { user.manager == true }
|
175
|
+
|
176
|
+
user.default_name = "Changed"
|
177
|
+
user.reset_default_name!
|
178
|
+
assert { user.default_name == "Foo" }
|
179
|
+
|
180
|
+
assert { user.raw_value == 1 }
|
181
|
+
user.raw_value = "raw"
|
182
|
+
assert { user.raw_value == "raw" }
|
183
|
+
|
184
|
+
user.raw_value2 = "raw_value2"
|
185
|
+
assert { user.raw_value2 == "raw_value2" }
|
186
|
+
|
187
|
+
assert { user.raw_value3 == "raw_value3" }
|
188
|
+
user.raw_value3 = 3.0
|
189
|
+
assert { user.raw_value3 > 2.9 }
|
190
|
+
|
191
|
+
assert { user.skills == [] }
|
192
|
+
user.skills = "ruby"
|
193
|
+
assert { user.skills == ["ruby"] }
|
194
|
+
user.skills = 1
|
195
|
+
assert { user.skills == ["1"] }
|
196
|
+
user.skills = ["ruby", "js", 1, [2]]
|
197
|
+
assert { user.skills == ["ruby", "js", "1", "[2]"] }
|
198
|
+
|
199
|
+
assert { user.friend_ids.nil? }
|
200
|
+
user.friend_ids = 1
|
201
|
+
assert { user.friend_ids == [1] }
|
202
|
+
user.friend_ids = ["1", 2, 3]
|
203
|
+
assert { user.friend_ids == [1, 2, 3] }
|
204
|
+
|
205
|
+
user.nested_array = 1
|
206
|
+
assert { user.nested_array == [[1]] }
|
207
|
+
user.nested_array = [2]
|
208
|
+
assert { user.nested_array == [[2]] }
|
209
|
+
user.nested_array = [[2, "3"]]
|
210
|
+
assert { user.nested_array == [[2, 3]] }
|
211
|
+
|
212
|
+
user2 = User.new(name: 20)
|
213
|
+
assert { user2.name == "20" }
|
214
|
+
assert { user2.default_name == "Foo" }
|
215
|
+
assert { user2.conditional_default == "Hello 20" }
|
216
|
+
user2.conditional_default = "hoge"
|
217
|
+
assert { user2.conditional_default == "hoge" }
|
218
|
+
user2.reset_conditional_default!
|
219
|
+
assert { user2.conditional_default == "Hello 20" }
|
220
|
+
|
221
|
+
user3 = User.new(default_name: "kakyoin")
|
222
|
+
assert { user3.name.nil? }
|
223
|
+
assert { user3.default_name == "kakyoin" }
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
require 'coveralls'
|
3
|
+
Coveralls.wear!
|
4
|
+
|
5
|
+
require 'attr_typecastable'
|
6
|
+
|
7
|
+
require 'rspec-power_assert'
|
8
|
+
RSpec::PowerAssert.example_assertion_alias :assert
|
9
|
+
RSpec::PowerAssert.example_group_assertion_alias :assert
|
metadata
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: attr_typecastable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- joker1007
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: coveralls
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: attr_accessor with typecast feature.
|
84
|
+
email:
|
85
|
+
- kakyoin.hierophant@gmail.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- ".rspec"
|
92
|
+
- ".travis.yml"
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- attr_typecastable.gemspec
|
98
|
+
- lib/attr_typecastable.rb
|
99
|
+
- lib/attr_typecastable/initialize_hook.rb
|
100
|
+
- lib/attr_typecastable/reflection.rb
|
101
|
+
- lib/attr_typecastable/types.rb
|
102
|
+
- lib/attr_typecastable/types/array.rb
|
103
|
+
- lib/attr_typecastable/types/base.rb
|
104
|
+
- lib/attr_typecastable/types/big_decimal.rb
|
105
|
+
- lib/attr_typecastable/types/boolean.rb
|
106
|
+
- lib/attr_typecastable/types/date.rb
|
107
|
+
- lib/attr_typecastable/types/date_time.rb
|
108
|
+
- lib/attr_typecastable/types/exception.rb
|
109
|
+
- lib/attr_typecastable/types/float.rb
|
110
|
+
- lib/attr_typecastable/types/integer.rb
|
111
|
+
- lib/attr_typecastable/types/object.rb
|
112
|
+
- lib/attr_typecastable/types/rational.rb
|
113
|
+
- lib/attr_typecastable/types/string.rb
|
114
|
+
- lib/attr_typecastable/types/symbol.rb
|
115
|
+
- lib/attr_typecastable/types/time.rb
|
116
|
+
- lib/attr_typecastable/version.rb
|
117
|
+
- spec/attr_typecastable_spec.rb
|
118
|
+
- spec/performance_check.rb
|
119
|
+
- spec/spec_helper.rb
|
120
|
+
homepage: https://github.com/joker1007/attr_typecastable
|
121
|
+
licenses:
|
122
|
+
- MIT
|
123
|
+
metadata: {}
|
124
|
+
post_install_message:
|
125
|
+
rdoc_options: []
|
126
|
+
require_paths:
|
127
|
+
- lib
|
128
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
requirements: []
|
139
|
+
rubyforge_project:
|
140
|
+
rubygems_version: 2.4.1
|
141
|
+
signing_key:
|
142
|
+
specification_version: 4
|
143
|
+
summary: attr_accessor with typecast feature.
|
144
|
+
test_files:
|
145
|
+
- spec/attr_typecastable_spec.rb
|
146
|
+
- spec/performance_check.rb
|
147
|
+
- spec/spec_helper.rb
|