calculated_attributes 0.0.22 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f5d1e1c6a85809abd342ec35a184d0ae2f7a18d2
4
- data.tar.gz: f294bd5a311c92f6c4b14d6b4ed532c5a13117c6
3
+ metadata.gz: c8141eabe21755f5e75184d22da98d5368e6c165
4
+ data.tar.gz: 18f6d50e0ed85de3aebb5e52a5cfe194b7c33fc2
5
5
  SHA512:
6
- metadata.gz: 72d6a5a63cdb53272e9d3cab73945fb91b3d23a7bc09ac5588195d0351bb3f4d77d4d93645dcba01f7d44657a6717acfae266c57762269edb118bd16e4f6fd88
7
- data.tar.gz: 1e4e6d4ab5b312e99c7ec28df1f8710fb08b8521aa7ccda3aa312c99b2e6344626a58261c1bb9be09efea51be60c62ca89719852beda27945a832d0b79de70f9
6
+ metadata.gz: 8bd4c465bef83c4bfc3ad161a2095bc2908c08cb05db4638b0c9398355063b60a6a5dc7ecd7a5826a9ebe4553db42072396cc4fd8649d84bf9daabddb574558f
7
+ data.tar.gz: 24bb795211e8ef9fb38728aea7c40560ac3b3ccbc3ec8029e4e4755f43dea633564b59a20b0b46bf60bc57aa82713ff371afc6c9dfe91eda7991d8952935a22f
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Gem Version](https://badge.fury.io/rb/calculated_attributes.svg)](https://badge.fury.io/rb/calculated_attributes)
2
+
1
3
  # CalculatedAttributes
2
4
 
3
5
  Automatically add calculated attributes from accessory select queries to ActiveRecord models.
@@ -0,0 +1,5 @@
1
+ Arel::SelectManager.send(:include, Module.new do
2
+ def projections
3
+ @ctx.projections
4
+ end
5
+ end)
@@ -0,0 +1,79 @@
1
+ module CalculatedAttributes
2
+ def calculated(*args)
3
+ @config ||= CalculatedAttributes::Config.new
4
+ @config.calculated(args.first, args.last) if args.size == 2
5
+ @config
6
+ end
7
+
8
+ class CalculatedAttributes
9
+ class Config
10
+ def calculated(title = nil, lambda = nil)
11
+ @calculations ||= {}
12
+ @calculations[title] ||= lambda if title && lambda
13
+ @calculations
14
+ end
15
+ end
16
+ end
17
+ end
18
+ ActiveRecord::Base.extend CalculatedAttributes
19
+
20
+ ActiveRecord::Base.send(:include, Module.new do
21
+ def calculated(*args)
22
+ if self.class.respond_to? :scoped
23
+ self.class.scoped.calculated(*args).find(id)
24
+ else
25
+ self.class.all.calculated(*args).find(id)
26
+ end
27
+ end
28
+
29
+ def method_missing(sym, *args, &block)
30
+ no_sym_in_attr =
31
+ if @attributes.respond_to? :include?
32
+ !@attributes.include?(sym.to_s)
33
+ else
34
+ !@attributes.key?(sym.to_s)
35
+ end
36
+ if no_sym_in_attr && (self.class.calculated.calculated[sym] || self.class.base_class.calculated.calculated[sym])
37
+ Rails.logger.warn("Using calculated value without including it in the relation: #{sym}") if defined? Rails
38
+ class_with_attr =
39
+ if self.class.calculated.calculated[sym]
40
+ self.class
41
+ else
42
+ self.class.base_class
43
+ end
44
+ if class_with_attr.respond_to? :scoped
45
+ class_with_attr.scoped.calculated(sym).find(id).send(sym)
46
+ else
47
+ class_with_attr.all.calculated(sym).find(id).send(sym)
48
+ end
49
+ else
50
+ super(sym, *args, &block)
51
+ end
52
+ end
53
+
54
+ def respond_to?(method, include_private = false)
55
+ no_sym_in_attr =
56
+ if @attributes.respond_to? :include?
57
+ !@attributes.include?(method.to_s)
58
+ elsif @attributes.respond_to? :key?
59
+ !@attributes.key?(method.to_s)
60
+ else
61
+ true
62
+ end
63
+ super || (no_sym_in_attr && (self.class.calculated.calculated[method] || self.class.base_class.calculated.calculated[method]))
64
+ end
65
+ end)
66
+
67
+ ActiveRecord::Relation.send(:include, Module.new do
68
+ def calculated(*args)
69
+ projections = arel.projections
70
+ args.each do |arg|
71
+ lam = klass.calculated.calculated[arg] || klass.base_class.calculated.calculated[arg]
72
+ sql = lam.call
73
+ new_projection = sql.is_a?(String) ? Arel.sql("(#{sql})").as(arg.to_s) : sql.as(arg.to_s)
74
+ new_projection.calculated_attr!
75
+ projections.push new_projection
76
+ end
77
+ select(projections)
78
+ end
79
+ end)
@@ -0,0 +1,70 @@
1
+ module ActiveRecord
2
+ module AttributeMethods
3
+ module ClassMethods
4
+ # Generates all the attribute related methods for columns in the database
5
+ # accessors, mutators and query methods.
6
+ def define_attribute_methods
7
+ unless defined?(@attribute_methods_mutex)
8
+ msg = 'It looks like something (probably a gem/plugin) is overriding the ' \
9
+ 'ActiveRecord::Base.inherited method. It is important that this hook executes so ' \
10
+ 'that your models are set up correctly. A workaround has been added to stop this ' \
11
+ 'causing an error in 3.2, but future versions will simply not work if the hook is ' \
12
+ 'overridden. If you are using Kaminari, please upgrade as it is known to have had ' \
13
+ "this problem.\n\n"
14
+ msg << 'The following may help track down the problem:'
15
+
16
+ meth = method(:inherited)
17
+ if meth.respond_to?(:source_location)
18
+ msg << " #{meth.source_location.inspect}"
19
+ else
20
+ msg << " #{meth.inspect}"
21
+ end
22
+ msg << "\n\n"
23
+
24
+ ActiveSupport::Deprecation.warn(msg)
25
+
26
+ @attribute_methods_mutex = Mutex.new
27
+ end
28
+
29
+ # Use a mutex; we don't want two thread simaltaneously trying to define
30
+ # attribute methods.
31
+ @attribute_methods_mutex.synchronize do
32
+ return if attribute_methods_generated?
33
+ superclass.define_attribute_methods unless self == base_class
34
+ columns_to_define =
35
+ if defined?(calculated) && calculated.instance_variable_get('@calculations')
36
+ calculated_keys = calculated.instance_variable_get('@calculations').keys
37
+ column_names.reject { |c| calculated_keys.include? c.intern }
38
+ else
39
+ column_names
40
+ end
41
+ super(columns_to_define)
42
+ columns_to_define.each { |name| define_external_attribute_method(name) }
43
+ @attribute_methods_generated = true
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ module Associations
50
+ class JoinDependency
51
+ attr_writer :calculated_columns
52
+
53
+ def instantiate(rows)
54
+ primary_key = join_base.aliased_primary_key
55
+ parents = {}
56
+
57
+ records = rows.map do |model|
58
+ primary_id = model[primary_key]
59
+ parent = parents[primary_id] ||= join_base.instantiate(model)
60
+ construct(parent, @associations, join_associations, model)
61
+ @calculated_columns.each { |column| parent[column.right] = model[column.right] }
62
+ parent
63
+ end.uniq
64
+
65
+ remove_duplicate_results!(active_record, records, @associations)
66
+ records
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,64 @@
1
+ module ActiveRecord
2
+ module AttributeMethods
3
+ module ClassMethods
4
+ # Generates all the attribute related methods for columns in the database
5
+ # accessors, mutators and query methods.
6
+ def define_attribute_methods
7
+ return false if @attribute_methods_generated
8
+ # Use a mutex; we don't want two threads simultaneously trying to define
9
+ # attribute methods.
10
+ generated_attribute_methods.synchronize do
11
+ return false if @attribute_methods_generated
12
+ superclass.define_attribute_methods unless self == base_class
13
+ columns_to_define =
14
+ if defined?(calculated) && calculated.instance_variable_get('@calculations')
15
+ calculated_keys = calculated.instance_variable_get('@calculations').keys
16
+ column_names.reject { |c| calculated_keys.include? c.intern }
17
+ else
18
+ column_names
19
+ end
20
+ super(columns_to_define)
21
+ @attribute_methods_generated = true
22
+ end
23
+ true
24
+ end
25
+ end
26
+ end
27
+
28
+ module Associations
29
+ class JoinDependency
30
+ attr_writer :calculated_columns
31
+
32
+ def instantiate(result_set, aliases)
33
+ primary_key = aliases.column_alias(join_root, join_root.primary_key)
34
+
35
+ seen = Hash.new do |i, object_id|
36
+ i[object_id] = Hash.new do |j, child_class|
37
+ j[child_class] = {}
38
+ end
39
+ end
40
+
41
+ model_cache = Hash.new { |h, klass| h[klass] = {} }
42
+ parents = model_cache[join_root]
43
+ column_aliases = aliases.column_aliases join_root
44
+
45
+ message_bus = ActiveSupport::Notifications.instrumenter
46
+
47
+ payload = {
48
+ record_count: result_set.length,
49
+ class_name: join_root.base_klass.name
50
+ }
51
+
52
+ message_bus.instrument('instantiation.active_record', payload) do
53
+ result_set.each do |row_hash|
54
+ parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases)
55
+ @calculated_columns.each { |column| parent[column.right] = model[column.right] }
56
+ construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
57
+ end
58
+ end
59
+
60
+ parents.values
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,45 @@
1
+ module ActiveRecord
2
+ module FinderMethods
3
+ def construct_relation_for_association_find(join_dependency)
4
+ calculated_columns = arel.projections.select { |p| p.is_a?(Arel::Nodes::Node) && p.calculated_attr? }
5
+ relation = except(:includes, :eager_load, :preload, :select).select(join_dependency.columns.concat(calculated_columns))
6
+ join_dependency.calculated_columns = calculated_columns
7
+ apply_join_dependency(relation, join_dependency)
8
+ end
9
+ end
10
+ end
11
+
12
+ module ActiveRecord
13
+ module AttributeMethods
14
+ module Write
15
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings
16
+ # for fixnum and float columns are turned into +nil+.
17
+ def write_attribute(attr_name, value)
18
+ if ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR == 2
19
+ write_attribute_with_type_cast(attr_name, value, true)
20
+ else
21
+ attr_name = attr_name.to_s
22
+ attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
23
+ @attributes_cache.delete(attr_name)
24
+ column = column_for_attribute(attr_name)
25
+
26
+ @attributes[attr_name] = type_cast_attribute_for_write(column, value)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ module Arel
34
+ module Nodes
35
+ class Node
36
+ def calculated_attr!
37
+ @is_calculated_attr = true
38
+ end
39
+
40
+ def calculated_attr?
41
+ @is_calculated_attr
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,3 +1,3 @@
1
1
  module CalculatedAttributes
2
- VERSION = '0.0.22'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -1,226 +1,11 @@
1
1
  require 'calculated_attributes/version'
2
2
  require 'active_record'
3
3
 
4
- module CalculatedAttributes
5
- def calculated(*args)
6
- @config ||= CalculatedAttributes::Config.new
7
- @config.calculated(args.first, args.last) if args.size == 2
8
- @config
9
- end
10
-
11
- class CalculatedAttributes
12
- class Config
13
- def calculated(title = nil, lambda = nil)
14
- @calculations ||= {}
15
- @calculations[title] ||= lambda if title && lambda
16
- @calculations
17
- end
18
- end
19
- end
20
- end
21
- ActiveRecord::Base.extend CalculatedAttributes
22
-
23
- ActiveRecord::Base.send(:include, Module.new do
24
- def calculated(*args)
25
- if self.class.respond_to? :scoped
26
- self.class.scoped.calculated(*args).find(id)
27
- else
28
- self.class.all.calculated(*args).find(id)
29
- end
30
- end
31
-
32
- def method_missing(sym, *args, &block)
33
- no_sym_in_attr =
34
- if @attributes.respond_to? :include?
35
- !@attributes.include?(sym.to_s)
36
- else
37
- !@attributes.key?(sym.to_s)
38
- end
39
- if no_sym_in_attr && (self.class.calculated.calculated[sym] || self.class.base_class.calculated.calculated[sym])
40
- Rails.logger.warn("Using calculated value without including it in the relation: #{sym}") if defined? Rails
41
- class_with_attr =
42
- if self.class.calculated.calculated[sym]
43
- self.class
44
- else
45
- self.class.base_class
46
- end
47
- if class_with_attr.respond_to? :scoped
48
- class_with_attr.scoped.calculated(sym).find(id).send(sym)
49
- else
50
- class_with_attr.all.calculated(sym).find(id).send(sym)
51
- end
52
- else
53
- super(sym, *args, &block)
54
- end
55
- end
56
-
57
- def respond_to?(method, include_private = false)
58
- no_sym_in_attr =
59
- if @attributes.respond_to? :include?
60
- !@attributes.include?(method.to_s)
61
- elsif @attributes.respond_to? :key?
62
- !@attributes.key?(method.to_s)
63
- else
64
- true
65
- end
66
- super || (no_sym_in_attr && (self.class.calculated.calculated[method] || self.class.base_class.calculated.calculated[method]))
67
- end
68
- end)
69
-
70
- ActiveRecord::Relation.send(:include, Module.new do
71
- def calculated(*args)
72
- projections = arel.projections
73
- args.each do |arg|
74
- lam = klass.calculated.calculated[arg] || klass.base_class.calculated.calculated[arg]
75
- sql = lam.call
76
- new_projection = sql.is_a?(String) ? Arel.sql("(#{sql})").as(arg.to_s) : sql.as(arg.to_s)
77
- new_projection.calculated_attr!
78
- projections.push new_projection
79
- end
80
- select(projections)
81
- end
82
- end)
83
-
84
- Arel::SelectManager.send(:include, Module.new do
85
- def projections
86
- @ctx.projections
87
- end
88
- end)
89
-
90
- module ActiveRecord
91
- module FinderMethods
92
- def construct_relation_for_association_find(join_dependency)
93
- calculated_columns = arel.projections.select { |p| p.is_a?(Arel::Nodes::Node) && p.calculated_attr? }
94
- relation = except(:includes, :eager_load, :preload, :select).select(join_dependency.columns.concat(calculated_columns))
95
- join_dependency.calculated_columns = calculated_columns
96
- apply_join_dependency(relation, join_dependency)
97
- end
98
- end
99
-
100
- module AttributeMethods
101
- module ClassMethods
102
- # Generates all the attribute related methods for columns in the database
103
- # accessors, mutators and query methods.
104
- def define_attribute_methods
105
- case ActiveRecord::VERSION::MAJOR
106
- when 3
107
- unless defined?(@attribute_methods_mutex)
108
- msg = "It looks like something (probably a gem/plugin) is overriding the " \
109
- "ActiveRecord::Base.inherited method. It is important that this hook executes so " \
110
- "that your models are set up correctly. A workaround has been added to stop this " \
111
- "causing an error in 3.2, but future versions will simply not work if the hook is " \
112
- "overridden. If you are using Kaminari, please upgrade as it is known to have had " \
113
- "this problem.\n\n"
114
- msg << "The following may help track down the problem:"
115
-
116
- meth = method(:inherited)
117
- if meth.respond_to?(:source_location)
118
- msg << " #{meth.source_location.inspect}"
119
- else
120
- msg << " #{meth.inspect}"
121
- end
122
- msg << "\n\n"
123
-
124
- ActiveSupport::Deprecation.warn(msg)
125
-
126
- @attribute_methods_mutex = Mutex.new
127
- end
128
-
129
- # Use a mutex; we don't want two thread simaltaneously trying to define
130
- # attribute methods.
131
- @attribute_methods_mutex.synchronize do
132
- return if attribute_methods_generated?
133
- superclass.define_attribute_methods unless self == base_class
134
- columns_to_define =
135
- if defined?(calculated) && calculated.instance_variable_get('@calculations')
136
- calculated_keys = calculated.instance_variable_get('@calculations').keys
137
- column_names.reject { |c| calculated_keys.include? c.intern }
138
- else
139
- column_names
140
- end
141
- super(columns_to_define)
142
- columns_to_define.each { |name| define_external_attribute_method(name) }
143
- @attribute_methods_generated = true
144
- end
145
-
146
- when 4
147
- return false if @attribute_methods_generated
148
- # Use a mutex; we don't want two threads simultaneously trying to define
149
- # attribute methods.
150
- generated_attribute_methods.synchronize do
151
- return false if @attribute_methods_generated
152
- superclass.define_attribute_methods unless self == base_class
153
- columns_to_define =
154
- if defined?(calculated) && calculated.instance_variable_get('@calculations')
155
- calculated_keys = calculated.instance_variable_get('@calculations').keys
156
- column_names.reject { |c| calculated_keys.include? c.intern }
157
- else
158
- column_names
159
- end
160
- super(columns_to_define)
161
- @attribute_methods_generated = true
162
- end
163
- true
164
- end
165
- end
166
- end
167
- end
168
-
169
- module Associations
170
- class JoinDependency
171
- attr_writer :calculated_columns
172
-
173
- def instantiate(rows)
174
- primary_key = join_base.aliased_primary_key
175
- parents = {}
176
-
177
- records = rows.map do |model|
178
- primary_id = model[primary_key]
179
- parent = parents[primary_id] ||= join_base.instantiate(model)
180
- construct(parent, @associations, join_associations, model)
181
- @calculated_columns.each { |column| parent[column.right] = model[column.right] }
182
- parent
183
- end.uniq
184
-
185
- remove_duplicate_results!(active_record, records, @associations)
186
- records
187
- end
188
- end
189
- end
190
- end
191
-
192
- module ActiveRecord
193
- module AttributeMethods
194
- module Write
195
- # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings
196
- # for fixnum and float columns are turned into +nil+.
197
- def write_attribute(attr_name, value)
198
- if ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR == 2
199
- write_attribute_with_type_cast(attr_name, value, true)
200
- else
201
- attr_name = attr_name.to_s
202
- attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
203
- @attributes_cache.delete(attr_name)
204
- column = column_for_attribute(attr_name)
205
-
206
- @attributes[attr_name] = type_cast_attribute_for_write(column, value)
207
- end
208
- end
209
- end
210
- end
211
- end
212
-
213
-
214
- module Arel
215
- module Nodes
216
- class Node
217
- def calculated_attr!
218
- @is_calculated_attr = true
219
- end
220
-
221
- def calculated_attr?
222
- @is_calculated_attr
223
- end
224
- end
225
- end
226
- end
4
+ # Include patches.
5
+ require 'calculated_attributes/rails_patches'
6
+ require 'calculated_attributes/arel_patches'
7
+ fail "Unsupported ActiveRecord version: #{ActiveRecord::VERSION::MAJOR}" unless [3, 4].include? ActiveRecord::VERSION::MAJOR
8
+ require "calculated_attributes/rails_#{ActiveRecord::VERSION::MAJOR}_patches"
9
+
10
+ # Include model code.
11
+ require 'calculated_attributes/model_methods'
data/spec/spec_helper.rb CHANGED
@@ -3,6 +3,4 @@ require 'calculated_attributes'
3
3
  ActiveRecord::Base.establish_connection(adapter: 'sqlite3',
4
4
  database: File.dirname(__FILE__) + '/calculated_attributes.sqlite3')
5
5
 
6
- load File.dirname(__FILE__) + '/support/schema.rb'
7
- load File.dirname(__FILE__) + '/support/models.rb'
8
- load File.dirname(__FILE__) + '/support/data.rb'
6
+ %w(schema models data).each { |f| load File.dirname(__FILE__) + "/support/#{f}.rb" }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: calculated_attributes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.22
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zach Schneider
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-07-01 00:00:00.000000000 Z
11
+ date: 2015-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: appraisal
@@ -131,6 +131,11 @@ files:
131
131
  - gemfiles/rails4_2.gemfile
132
132
  - gemfiles/rails4_2.gemfile.lock
133
133
  - lib/calculated_attributes.rb
134
+ - lib/calculated_attributes/arel_patches.rb
135
+ - lib/calculated_attributes/model_methods.rb
136
+ - lib/calculated_attributes/rails_3_patches.rb
137
+ - lib/calculated_attributes/rails_4_patches.rb
138
+ - lib/calculated_attributes/rails_patches.rb
134
139
  - lib/calculated_attributes/version.rb
135
140
  - spec/lib/calculated_attributes_spec.rb
136
141
  - spec/spec_helper.rb