fast_attributes_rails 1.0.0

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/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