fast_attributes_rails 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ ruby '2.1.2'
4
+
5
+ gem 'fast_attributes', path: '..'
6
+ gem 'virtus', '1.0.2'
7
+ gem 'attrio', '0.7.3'
8
+ gem 'benchmark-ips', '2.0.0'
@@ -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,3 @@
1
+ module FastAttributes
2
+ VERSION = '1.0.0'
3
+ 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