calculated_attributes 0.0.22 → 0.1.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.
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