fast_attributes_rails 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +194 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +292 -0
- data/Rakefile +1 -0
- data/benchmarks/Gemfile +8 -0
- data/benchmarks/comparison.rb +56 -0
- data/fast_attributes.gemspec +30 -0
- data/lib/fast_attributes.rb +83 -0
- data/lib/fast_attributes/builder.rb +125 -0
- data/lib/fast_attributes/default_attributes.rb +33 -0
- data/lib/fast_attributes/type_cast.rb +81 -0
- data/lib/fast_attributes/version.rb +3 -0
- data/spec/fast_attributes/type_cast_spec.rb +170 -0
- data/spec/fast_attributes_spec.rb +423 -0
- data/spec/fixtures/classes.rb +177 -0
- data/spec/spec_helper.rb +18 -0
- metadata +159 -0
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/benchmarks/Gemfile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'benchmark/ips'
|
2
|
+
require 'fast_attributes'
|
3
|
+
require 'virtus'
|
4
|
+
require 'attrio'
|
5
|
+
|
6
|
+
ATTR_NAMES = [:attr0, :attr1, :attr2, :attr3, :attr4, :attr5, :attr6, :attr7, :attr8, :attr9]
|
7
|
+
|
8
|
+
class FastIntegers
|
9
|
+
extend FastAttributes
|
10
|
+
define_attributes initialize: true do
|
11
|
+
attribute *ATTR_NAMES, Integer
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class AttrioIntegers
|
16
|
+
include Attrio
|
17
|
+
define_attributes do
|
18
|
+
ATTR_NAMES.each do |name|
|
19
|
+
attr name, Integer
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(attributes = {})
|
24
|
+
attributes.each do |attribute, value|
|
25
|
+
public_send("#{attribute}=", value)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class VirtusIntegers
|
31
|
+
include Virtus.model
|
32
|
+
ATTR_NAMES.each do |name|
|
33
|
+
attribute name, Integer
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
Benchmark.ips do |x|
|
38
|
+
x.config(time: 1, warmup: 1)
|
39
|
+
|
40
|
+
integers = {attr0: 0, attr1: 1, attr2: 2, attr3: 3, attr4: 4, attr5: 5, attr6: 6, attr7: 7, attr8: 8, attr9: 9}
|
41
|
+
strings = {attr0: '0', attr1: '1', attr2: '2', attr3: '3', attr4: '4', attr5: '5', attr6: '6', attr7: '7', attr8: '8', attr9: '9'}
|
42
|
+
|
43
|
+
x.report('FastAttributes: without values ') { FastIntegers.new }
|
44
|
+
x.report('FastAttributes: integer values for integer attributes') { FastIntegers.new(integers) }
|
45
|
+
x.report('FastAttributes: string values for integer attributes ') { FastIntegers.new(strings) }
|
46
|
+
|
47
|
+
x.report('Attrio: without values ') { AttrioIntegers.new }
|
48
|
+
x.report('Attrio: integer values for integer attributes ') { AttrioIntegers.new(integers) }
|
49
|
+
x.report('Attrio: string values for integer attributes ') { AttrioIntegers.new(strings) }
|
50
|
+
|
51
|
+
x.report('Virtus: without values ') { VirtusIntegers.new }
|
52
|
+
x.report('Virtus: integer values for integer attributes ') { VirtusIntegers.new(integers) }
|
53
|
+
x.report('Virtus: string values for integer attributes ') { VirtusIntegers.new(strings) }
|
54
|
+
|
55
|
+
x.compare!
|
56
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'fast_attributes/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'fast_attributes_rails'
|
8
|
+
spec.version = FastAttributes::VERSION
|
9
|
+
spec.authors = ['Damian Baćkowski']
|
10
|
+
spec.email = ['damianbackowski@gmail.com']
|
11
|
+
spec.summary = 'Fast attributes with data types'
|
12
|
+
spec.description = 'Fast attributes with data types'
|
13
|
+
spec.homepage = 'https://github.com/dbackowski/fast_attributes'
|
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.required_ruby_version = '>= 1.9.2'
|
22
|
+
|
23
|
+
spec.add_dependency 'activesupport', '>= 3.0.0', '< 6.0'
|
24
|
+
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.5'
|
26
|
+
spec.add_development_dependency 'rake'
|
27
|
+
spec.add_development_dependency 'rspec', '~> 3.0.0'
|
28
|
+
spec.add_development_dependency 'coveralls', '~> 0.7.0'
|
29
|
+
spec.add_development_dependency 'simplecov', '~> 0.8.2'
|
30
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'date'
|
3
|
+
require 'time'
|
4
|
+
require 'fast_attributes/version'
|
5
|
+
require 'fast_attributes/builder'
|
6
|
+
require 'fast_attributes/type_cast'
|
7
|
+
require 'fast_attributes/default_attributes'
|
8
|
+
require 'active_support/core_ext/hash'
|
9
|
+
|
10
|
+
module FastAttributes
|
11
|
+
TRUE_VALUES = {true => nil, 1 => nil, '1' => nil, 't' => nil, 'T' => nil, 'true' => nil, 'TRUE' => nil, 'on' => nil, 'ON' => nil}
|
12
|
+
FALSE_VALUES = {false => nil, 0 => nil, '0' => nil, 'f' => nil, 'F' => nil, 'false' => nil, 'FALSE' => nil, 'off' => nil, 'OFF' => nil}
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def type_casting
|
16
|
+
@type_casting ||= {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_type_casting(klass)
|
20
|
+
type_casting[klass]
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_type_casting(klass, casting)
|
24
|
+
symbol = klass.name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym # DateTime => :date_time
|
25
|
+
type_cast symbol, klass do
|
26
|
+
from 'nil', to: 'nil'
|
27
|
+
from klass.name, to: '%s'
|
28
|
+
otherwise casting
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def remove_type_casting(klass)
|
33
|
+
type_casting.delete(klass)
|
34
|
+
end
|
35
|
+
|
36
|
+
def type_exists?(klass)
|
37
|
+
type_casting.has_key?(klass)
|
38
|
+
end
|
39
|
+
|
40
|
+
def type_cast(*types_or_classes, &block)
|
41
|
+
types_or_classes.each do |type_or_class|
|
42
|
+
type_cast = TypeCast.new(type_or_class)
|
43
|
+
type_cast.instance_eval(&block)
|
44
|
+
type_casting[type_or_class] = type_cast
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def define_attributes(options = {}, &block)
|
50
|
+
builder = Builder.new(self, options)
|
51
|
+
builder.instance_eval(&block)
|
52
|
+
builder.compile!
|
53
|
+
end
|
54
|
+
|
55
|
+
def attribute(*attributes, type)
|
56
|
+
builder = Builder.new(self)
|
57
|
+
builder.attribute *attributes, type
|
58
|
+
builder.compile!
|
59
|
+
end
|
60
|
+
|
61
|
+
set_type_casting String, 'String(%s)'
|
62
|
+
set_type_casting Integer, 'Integer(%s)'
|
63
|
+
set_type_casting Float, 'Float(%s)'
|
64
|
+
set_type_casting Array, 'Array(%s)'
|
65
|
+
set_type_casting Date, 'Date.parse(%s)'
|
66
|
+
set_type_casting Time, 'Time.parse(%s)'
|
67
|
+
set_type_casting DateTime, 'DateTime.parse(%s)'
|
68
|
+
set_type_casting BigDecimal, 'Float(%s);BigDecimal(%s.to_s)'
|
69
|
+
|
70
|
+
type_cast :boolean do
|
71
|
+
otherwise <<-EOS
|
72
|
+
if FastAttributes::TRUE_VALUES.has_key?(%s)
|
73
|
+
true
|
74
|
+
elsif FastAttributes::FALSE_VALUES.has_key?(%s)
|
75
|
+
false
|
76
|
+
elsif %s.nil?
|
77
|
+
nil
|
78
|
+
else
|
79
|
+
raise FastAttributes::TypeCast::InvalidValueError, %(Invalid value "\#{%s}" for attribute "%a" of type ":boolean")
|
80
|
+
end
|
81
|
+
EOS
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module FastAttributes
|
2
|
+
class UnsupportedTypeError < TypeError
|
3
|
+
end
|
4
|
+
|
5
|
+
class Builder
|
6
|
+
def initialize(klass, options = {})
|
7
|
+
@klass = klass
|
8
|
+
@options = options
|
9
|
+
@attributes = []
|
10
|
+
@methods = Module.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def attribute(*attributes, type, options)
|
14
|
+
unless options.is_a?(Hash)
|
15
|
+
(attributes ||= []) << type
|
16
|
+
type = options
|
17
|
+
options = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
unless FastAttributes.type_exists?(type)
|
21
|
+
raise UnsupportedTypeError, %(Unsupported attribute type "#{type.inspect}")
|
22
|
+
end
|
23
|
+
|
24
|
+
@attributes << [attributes, type, options || {}]
|
25
|
+
end
|
26
|
+
|
27
|
+
def compile!
|
28
|
+
compile_getter
|
29
|
+
compile_setter
|
30
|
+
set_defaults
|
31
|
+
|
32
|
+
if @options[:initialize]
|
33
|
+
compile_initialize
|
34
|
+
end
|
35
|
+
|
36
|
+
if @options[:attributes]
|
37
|
+
compile_attributes(@options[:attributes])
|
38
|
+
end
|
39
|
+
|
40
|
+
include_methods
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def compile_getter
|
46
|
+
each_attribute do |attribute, *|
|
47
|
+
@methods.module_eval <<-EOS, __FILE__, __LINE__ + 1
|
48
|
+
def #{attribute} # def name
|
49
|
+
@#{attribute} # @name
|
50
|
+
end # end
|
51
|
+
EOS
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def compile_setter
|
56
|
+
each_attribute do |attribute, type, *|
|
57
|
+
type_cast = FastAttributes.get_type_casting(type)
|
58
|
+
method_body = type_cast.compile_method_body(attribute, 'value')
|
59
|
+
|
60
|
+
@methods.module_eval <<-EOS, __FILE__, __LINE__ + 1
|
61
|
+
def #{attribute}=(value)
|
62
|
+
@#{attribute} = #{method_body}
|
63
|
+
end
|
64
|
+
EOS
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def compile_initialize
|
69
|
+
attribute_string = if FastAttributes.default_attributes(@klass).empty?
|
70
|
+
"attributes"
|
71
|
+
else
|
72
|
+
"FastAttributes.default_attributes(self.class).merge(attributes)"
|
73
|
+
end
|
74
|
+
|
75
|
+
@methods.module_eval <<-EOS, __FILE__, __LINE__ + 1
|
76
|
+
def initialize(attributes = {})
|
77
|
+
#{attribute_string}.each do |name, value|
|
78
|
+
public_send("\#{name}=", value) if respond_to?("\#{name}=".to_sym)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
EOS
|
82
|
+
end
|
83
|
+
|
84
|
+
def compile_attributes(mode)
|
85
|
+
attributes = @attributes.flat_map(&:first)
|
86
|
+
prefix = case mode
|
87
|
+
when :accessors then ''
|
88
|
+
else '@'
|
89
|
+
end
|
90
|
+
|
91
|
+
attributes = attributes.map do |attribute|
|
92
|
+
"'#{attribute}' => #{prefix}#{attribute}"
|
93
|
+
end
|
94
|
+
|
95
|
+
@methods.module_eval <<-EOS, __FILE__, __LINE__ + 1
|
96
|
+
def attributes # def attributes
|
97
|
+
{#{attributes.join(', ')}}.with_indifferent_access # {'name' => @name, ...}
|
98
|
+
end # end
|
99
|
+
EOS
|
100
|
+
end
|
101
|
+
|
102
|
+
def include_methods
|
103
|
+
@methods.instance_eval <<-EOS, __FILE__, __LINE__ + 1
|
104
|
+
def inspect
|
105
|
+
'FastAttributes(#{@attributes.flat_map(&:first).join(', ')})'
|
106
|
+
end
|
107
|
+
EOS
|
108
|
+
@klass.send(:include, @methods)
|
109
|
+
end
|
110
|
+
|
111
|
+
def set_defaults
|
112
|
+
each_attribute do |attribute, type, options|
|
113
|
+
FastAttributes.add_default_attribute(@klass, attribute, options[:default]) if options[:default]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def each_attribute
|
118
|
+
@attributes.each do |attributes, type, options = {}|
|
119
|
+
attributes.each do |attribute|
|
120
|
+
yield attribute, type, options
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module FastAttributes
|
2
|
+
class << self
|
3
|
+
SINGLETON_CLASSES = [::NilClass, ::TrueClass, ::FalseClass, ::Numeric, ::Symbol].freeze
|
4
|
+
|
5
|
+
def default_attributes(klass)
|
6
|
+
return {} unless (@default_attributes || {})[klass]
|
7
|
+
@default_attributes[klass].each_with_object({}) do |(attribute, value), memo|
|
8
|
+
memo[attribute] = if value.respond_to?(:call)
|
9
|
+
value.call
|
10
|
+
elsif cloneable?(value)
|
11
|
+
value.clone
|
12
|
+
else
|
13
|
+
value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_default_attribute(klass, attribute, value)
|
19
|
+
@default_attributes ||= {}
|
20
|
+
@default_attributes[klass] ||= {}
|
21
|
+
@default_attributes[klass][attribute] = value
|
22
|
+
end
|
23
|
+
|
24
|
+
def cloneable?(value)
|
25
|
+
case value
|
26
|
+
when *SINGLETON_CLASSES
|
27
|
+
false
|
28
|
+
else
|
29
|
+
true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module FastAttributes
|
2
|
+
class TypeCast
|
3
|
+
class UnknownTypeCastingError < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
class InvalidValueError < TypeError
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(type)
|
10
|
+
@type = type
|
11
|
+
@if_conditions = []
|
12
|
+
@else_condition = %q(raise FastAttributes::TypeCast::UnknownTypeCastingError, 'Type casting is not defined')
|
13
|
+
@rescue_conditions = nil
|
14
|
+
@default_rescue = %(raise FastAttributes::TypeCast::InvalidValueError, %(Invalid value "\#{%s}" for attribute "%a" of type "#{@type}"))
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def escape_template(template, attribute_name, argument_name)
|
19
|
+
template.gsub(/%+a|%+s/) do |match|
|
20
|
+
match.each_char.each_slice(2).map do |placeholder|
|
21
|
+
case placeholder
|
22
|
+
when %w[% a] then attribute_name
|
23
|
+
when %w[% s] then argument_name
|
24
|
+
when %w[% %] then '%'
|
25
|
+
else placeholder.join
|
26
|
+
end
|
27
|
+
end.join
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def from(condition, options = {})
|
33
|
+
@if_conditions << [condition, options[:to]]
|
34
|
+
end
|
35
|
+
|
36
|
+
def otherwise(else_condition)
|
37
|
+
@else_condition = else_condition
|
38
|
+
end
|
39
|
+
|
40
|
+
def on_error(error, options = {})
|
41
|
+
@rescue_conditions ||=[]
|
42
|
+
@rescue_conditions << [error, options[:act]]
|
43
|
+
end
|
44
|
+
|
45
|
+
def type_casting_template
|
46
|
+
@type_casting_template ||= begin
|
47
|
+
if @if_conditions.any?
|
48
|
+
conditions = @if_conditions.map do |from, to|
|
49
|
+
"when #{from}\n" +
|
50
|
+
" #{to}\n"
|
51
|
+
end
|
52
|
+
|
53
|
+
"case %s\n" +
|
54
|
+
conditions.join +
|
55
|
+
"else\n" +
|
56
|
+
" #{@else_condition}\n" +
|
57
|
+
"end"
|
58
|
+
else
|
59
|
+
@else_condition
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def rescue_template
|
65
|
+
rescues = @rescue_conditions || [['', @default_rescue]]
|
66
|
+
rescues.map do |error, action|
|
67
|
+
"rescue #{error} => e\n" +
|
68
|
+
" #{action}"
|
69
|
+
end.join("\n")
|
70
|
+
end
|
71
|
+
|
72
|
+
def compile_method_body(attribute_name, argument_name)
|
73
|
+
method_body = "begin\n" +
|
74
|
+
" #{type_casting_template}\n" +
|
75
|
+
"#{rescue_template}\n" +
|
76
|
+
"end"
|
77
|
+
|
78
|
+
self.class.escape_template(method_body, attribute_name, argument_name)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FastAttributes::TypeCast do
|
4
|
+
describe '.escape_template' do
|
5
|
+
it 'replaces placeholder with proper values' do
|
6
|
+
template = '% %s %%s %%%s %%%%s % %a %%a %%%a %%%%a'
|
7
|
+
escaped = FastAttributes::TypeCast.escape_template(template, 'price', 'arg')
|
8
|
+
expect(escaped).to eq('% arg %s %arg %%s % price %a %price %%a')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#type_casting_template' do
|
13
|
+
let(:type_cast) { FastAttributes::TypeCast.new(String) }
|
14
|
+
|
15
|
+
describe 'without any conditions' do
|
16
|
+
it 'return exception' do
|
17
|
+
expect(type_cast.type_casting_template.gsub(' ', '')).to eq <<-EOS.gsub(' ', '').chomp
|
18
|
+
raise FastAttributes::TypeCast::UnknownTypeCastingError, 'Type casting is not defined'
|
19
|
+
EOS
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'when one rule is defined' do
|
24
|
+
before do
|
25
|
+
type_cast.from 'nil', to: 'nil'
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'returns one when statement' do
|
29
|
+
expect(type_cast.type_casting_template.gsub(' ', '')).to eq <<-EOS.gsub(' ', '').chomp
|
30
|
+
case %s
|
31
|
+
when nil
|
32
|
+
nil
|
33
|
+
else
|
34
|
+
raise FastAttributes::TypeCast::UnknownTypeCastingError, 'Type casting is not defined'
|
35
|
+
end
|
36
|
+
EOS
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'when three rules are defined' do
|
41
|
+
before do
|
42
|
+
type_cast.from 'String', to: 'String(%s)'
|
43
|
+
type_cast.from 'Array', to: 'Array(%s)'
|
44
|
+
type_cast.from 'Integer', to: 'Integer(%s)'
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'returns three when statements' do
|
48
|
+
expect(type_cast.type_casting_template.gsub(' ', '')).to eq <<-EOS.gsub(' ', '').chomp
|
49
|
+
case %s
|
50
|
+
when String
|
51
|
+
String(%s)
|
52
|
+
when Array
|
53
|
+
Array(%s)
|
54
|
+
when Integer
|
55
|
+
Integer(%s)
|
56
|
+
else
|
57
|
+
raise FastAttributes::TypeCast::UnknownTypeCastingError, 'Type casting is not defined'
|
58
|
+
end
|
59
|
+
EOS
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe 'when 2 rules and otherwise condition are defined' do
|
64
|
+
before do
|
65
|
+
type_cast.from 'Date', to: 'Date.parse(%s)'
|
66
|
+
type_cast.from 'Time', to: 'Time.parse(%s)'
|
67
|
+
type_cast.otherwise 'Float(%s)'
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'returns 2 when statements and overrides default else condition' do
|
71
|
+
expect(type_cast.type_casting_template.gsub(' ', '')).to eq <<-EOS.gsub(' ', '').chomp
|
72
|
+
case %s
|
73
|
+
when Date
|
74
|
+
Date.parse(%s)
|
75
|
+
when Time
|
76
|
+
Time.parse(%s)
|
77
|
+
else
|
78
|
+
Float(%s)
|
79
|
+
end
|
80
|
+
EOS
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe 'when only otherwise rule is defined' do
|
85
|
+
before do
|
86
|
+
type_cast.otherwise '42 * %s'
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'returns otherwise statement only' do
|
90
|
+
expect(type_cast.type_casting_template.gsub(' ', '')).to eq <<-EOS.gsub(' ', '').chomp
|
91
|
+
42 * %s
|
92
|
+
EOS
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe '#rescue_template' do
|
98
|
+
let(:type_cast) { FastAttributes::TypeCast.new(Float) }
|
99
|
+
|
100
|
+
describe 'when on_error is not defined' do
|
101
|
+
it 'raises default error message' do
|
102
|
+
expect(type_cast.rescue_template.gsub(' ', '')).to eq <<-EOS.gsub(' ', '').chomp
|
103
|
+
rescue => e
|
104
|
+
raise FastAttributes::TypeCast::InvalidValueError, %(Invalid value "\#{%s}" for attribute "%a" of type "Float")
|
105
|
+
EOS
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe 'when on_error is defined' do
|
110
|
+
before do
|
111
|
+
type_cast.on_error 'ArgumentError', act: '"%s" + "%a"'
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'overrides default action' do
|
115
|
+
expect(type_cast.rescue_template.gsub(' ', '')).to eq <<-EOS.gsub(' ', '').chomp
|
116
|
+
rescue ArgumentError => e
|
117
|
+
"%s" + "%a"
|
118
|
+
EOS
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe 'when several on_error methods is called' do
|
123
|
+
before do
|
124
|
+
type_cast.on_error 'ArgumentError', act: '0'
|
125
|
+
type_cast.on_error 'StandardError', act: '1'
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'generates several rescue conditions' do
|
129
|
+
expect(type_cast.rescue_template.gsub(' ', '')).to eq <<-EOS.gsub(' ', '').chomp
|
130
|
+
rescue ArgumentError => e
|
131
|
+
0
|
132
|
+
rescue StandardError => e
|
133
|
+
1
|
134
|
+
EOS
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
describe '#compile_method_body' do
|
140
|
+
let(:type_cast) { FastAttributes::TypeCast.new(Float) }
|
141
|
+
|
142
|
+
before do
|
143
|
+
type_cast.from 'nil', to: 'nil'
|
144
|
+
type_cast.from 'Float', to: '%s'
|
145
|
+
type_cast.otherwise 'Float(%s)'
|
146
|
+
type_cast.on_error 'ArgumentError', act: 'raise "a %s %a"'
|
147
|
+
type_cast.on_error 'StandardError', act: 'raise "b %s %a"'
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
it 'generates type casting method' do
|
152
|
+
expect(type_cast.compile_method_body('price', 'argument').gsub(' ', '')).to eq <<-EOS.gsub(' ', '').chomp
|
153
|
+
begin
|
154
|
+
case argument
|
155
|
+
when nil
|
156
|
+
nil
|
157
|
+
when Float
|
158
|
+
argument
|
159
|
+
else
|
160
|
+
Float(argument)
|
161
|
+
end
|
162
|
+
rescue ArgumentError => e
|
163
|
+
raise "a argument price"
|
164
|
+
rescue StandardError => e
|
165
|
+
raise "b argument price"
|
166
|
+
end
|
167
|
+
EOS
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|