compute 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .idea
19
+ .rvmrc
20
+ .rspec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in compute.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Venkat Dinavahi
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # Compute
2
+
3
+ Compute is like Excel for your ActiveRecord models.
4
+ It lets you define computed attributes stored that get stored the database.
5
+
6
+ The main benefits are
7
+ - Performance: computed columns are only updated when the values they depend on change
8
+ - Querying: you can now easy include these columns in your queries
9
+
10
+ Here are some sample use cases
11
+ - Having an field which depends on the users birthday. Now you can easily query on age instead of having custom SQL.
12
+ - An SHA1 hash that gets updated when the a file path changes
13
+ - A bill could have tax, tip, and total which all depend on subtotal
14
+ - Having a city and state column get computed from longitude and latitude (using the Google Geocoding API)
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ gem 'compute'
21
+
22
+ And then execute:
23
+
24
+ $ bundle
25
+
26
+ Or install it yourself as:
27
+
28
+ $ gem install compute
29
+
30
+ ## Usage
31
+
32
+ The compute method accepts a block.
33
+ The names of the arguments in the block MUST match up with the properties of the model.
34
+ The following example keeps the user.age column in sync with the user.time column.
35
+
36
+ ```
37
+ class User < ActiveRecord::Base
38
+
39
+ compute :age do |birthday|
40
+ unless birthday.blank?
41
+ now = Time.now.utc.to_date
42
+ now.year - birthday.year - ((now.month > birthday.month || (now.month == birthday.month && now.day >= birthday.day)) ? 0 : 1)
43
+ end
44
+ end
45
+
46
+ end
47
+ ```
48
+
49
+ The compute method accepts multiple arguments. Computations are also run in the correct order.
50
+ In the block, self will be set to the model instance.
51
+ In the following example, total will be calculated after tax and tip are calculated. Again, think of it like Excel!
52
+
53
+ ```
54
+ class RestaurantBill < ActiveRecord::Base
55
+
56
+ compute(:date) { |created_at| created_at.to_date }
57
+
58
+ compute :total do |tax, tip|
59
+ tax + tip
60
+ end
61
+
62
+ compute :tax do |subtotal|
63
+ subtotal * tax_rate
64
+ end
65
+
66
+ compute :tip do |subtotal|
67
+ subtotal * tip_rate
68
+ end
69
+
70
+ def tax_rate
71
+ 0.08
72
+ end
73
+
74
+ def tip_rate
75
+ 0.15
76
+ end
77
+
78
+ end
79
+ ```
80
+
81
+ ## Contributing
82
+
83
+ 1. Fork it
84
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
85
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
86
+ 4. Push to the branch (`git push origin my-new-feature`)
87
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
8
+
data/compute.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/compute/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Venkat Dinavahi"]
6
+ gem.email = ["vendiddy@gmail.com"]
7
+ gem.description = %q{ActiveRecord extension for providing computed fields.}
8
+ gem.summary = %q{
9
+ Provides a compute DSL for creating ActiveRecord columns that are computed from other ones.
10
+ Automatically keeps the columns up to date.
11
+ }
12
+ gem.homepage = ""
13
+
14
+ gem.files = `git ls-files`.split($\)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.name = "compute"
18
+ gem.require_paths = ["lib"]
19
+ gem.version = Compute::VERSION
20
+ gem.required_ruby_version = '>= 1.9.3'
21
+
22
+ gem.add_dependency "activerecord"
23
+
24
+ gem.add_development_dependency "rspec"
25
+ gem.add_development_dependency "sqlite3"
26
+ gem.add_development_dependency "debugger"
27
+ gem.add_development_dependency "with_model"
28
+ end
data/lib/compute.rb ADDED
@@ -0,0 +1,57 @@
1
+ require "compute/version"
2
+ require "compute/railtie" if defined? Rails
3
+
4
+ require 'compute/computation'
5
+ require 'compute/computation_graph'
6
+
7
+ module Compute
8
+
9
+ module ClassMethods
10
+
11
+ def compute(property, &block)
12
+ computations << Computation.new(self, property, &block)
13
+ end
14
+
15
+ def computations
16
+ @computations ||= ComputationGraph.new
17
+ end
18
+
19
+ def recompute!(*properties)
20
+ scoped.each { |record| record.recompute!(*properties) }
21
+ end
22
+
23
+ end
24
+
25
+ def self.included(base)
26
+ base.extend ClassMethods
27
+ base.class_eval do
28
+ around_save :execute_outdated_computations
29
+ end
30
+ end
31
+
32
+ def recompute!(*properties)
33
+ properties = properties.compact.flatten
34
+ if properties.empty?
35
+ self.class.computations.each_in_order { |c| c.execute(self) }
36
+ else
37
+ properties.each do |property|
38
+ computation = self.class.computations.for_property(property)
39
+ computation.execute(self)
40
+ end
41
+ end
42
+ save
43
+ end
44
+
45
+ private
46
+
47
+ def execute_outdated_computations
48
+ yield
49
+
50
+ each_outdated_computation { |computation| computation.execute(self) }
51
+ end
52
+
53
+ def each_outdated_computation
54
+ self.class.computations.for_changed_properties(self.changed).each { |c| yield c }
55
+ end
56
+
57
+ end
@@ -0,0 +1,40 @@
1
+ module Compute
2
+
3
+ class Computation
4
+
5
+ attr_accessor :property
6
+
7
+ def initialize(model, property, &block)
8
+ @model = model
9
+ @property = property
10
+ @proc = Proc.new(&block)
11
+ end
12
+
13
+ def dependencies
14
+ @dependencies ||= calculate_dependencies(@proc)
15
+ end
16
+
17
+ def needs_update?(changed_properties)
18
+ common_changes = changed_properties.map(&:to_sym) & @dependencies
19
+ common_changes.count > 0
20
+ end
21
+
22
+ def execute(record)
23
+ source_values = source_values(record)
24
+ destination_result = record.instance_exec(*source_values, &@proc)
25
+ record.send(@property.to_s + '=', destination_result)
26
+ end
27
+
28
+ private
29
+
30
+ def calculate_dependencies(proc)
31
+ proc.parameters.map { |arg| arg[1] }
32
+ end
33
+
34
+ def source_values(record)
35
+ @dependencies.map { |property| record.send(property) }
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,70 @@
1
+ require 'tsort'
2
+
3
+ module Compute
4
+
5
+ class CyclicComputation < StandardError
6
+ end
7
+
8
+ class ComputationGraph < Hash
9
+ include TSort
10
+
11
+ def initialize
12
+ @sorted_computations = []
13
+ @computations_for_changed_properties = Hash.new do |h, changed_properties|
14
+ @sorted_computations.select { |c| c.needs_update?(changed_properties) }
15
+ end
16
+ end
17
+
18
+ def <<(computation)
19
+ self[computation.property] = computation
20
+ sort!
21
+ end
22
+
23
+ def each_in_order
24
+ @sorted_computations.each { |c| yield c }
25
+ end
26
+
27
+ def for_property(property)
28
+ self[property.to_sym]
29
+ end
30
+
31
+ def for_changed_properties(changed_properties)
32
+ @computations_for_changed_properties[changed_properties]
33
+ end
34
+
35
+ private
36
+
37
+ def sort!
38
+ begin
39
+ @sorted_computations = tsort.map { |p| self[p] }.compact
40
+ rescue TSort::Cyclic => e
41
+ raise CyclicComputation, "You have a circular dependency. (#{circular_dependency_string})"
42
+ end
43
+ end
44
+
45
+ def circular_dependency_string
46
+ circular_dependencies.map do |dependencies|
47
+ dependencies << dependencies.first
48
+ dependencies.join(' -> ')
49
+ end.join(', ')
50
+ end
51
+
52
+ def circular_dependencies
53
+ strongly_connected_components.select { |x| x.count > 1 }
54
+ end
55
+
56
+ def tsort_each_node
57
+ each do |_, computation|
58
+ yield computation.property
59
+ end
60
+ end
61
+
62
+ def tsort_each_child(property)
63
+ if self.has_key?(property)
64
+ self[property].dependencies.each { |p| yield p }
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,9 @@
1
+ module Compute
2
+ class Railtie < Rails::Railtie
3
+ initializer 'compute.model_additions' do
4
+ ActiveSupport.on_load :active_record do
5
+ ActiveRecord::Base.send :include, Compute
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module Compute
2
+ VERSION = "1.0.4"
3
+ end
@@ -0,0 +1,245 @@
1
+ require 'spec_helper'
2
+
3
+ describe Compute do
4
+
5
+ with_model :User do
6
+ table do |t|
7
+ t.string :first_name
8
+ t.string :last_name
9
+ t.string :full_name
10
+ t.string :first_initial
11
+ t.string :initials
12
+ t.date :date
13
+ t.timestamps
14
+ end
15
+
16
+ model do
17
+ include Compute
18
+
19
+ compute :first_initial do |first_name|
20
+ first_name[0]
21
+ end
22
+
23
+ compute :full_name do |first_name, last_name|
24
+ "#{first_name} #{last_name}"
25
+ end
26
+
27
+ compute :date do |created_at|
28
+ created_at.to_date
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+
35
+ with_model :Bill do
36
+ table do |t|
37
+ t.integer :subtotal
38
+ t.integer :tax
39
+ t.integer :tip
40
+ t.integer :total
41
+ end
42
+
43
+ model do
44
+ include Compute
45
+
46
+ # total is put first to ensure dependency tracker correctly sorts
47
+ compute :total do |subtotal, tax, tip|
48
+ subtotal + tax + tip
49
+ end
50
+
51
+ compute :tax do |subtotal|
52
+ (subtotal * 0.05).to_i
53
+ end
54
+
55
+ compute :tip do |subtotal|
56
+ (subtotal * 0.15).to_i
57
+ end
58
+ end
59
+ end
60
+
61
+ with_model :ModelWithCycles do
62
+
63
+ table do |t|
64
+ t.integer :a
65
+ t.integer :b
66
+ end
67
+
68
+ model do
69
+ include Compute
70
+ end
71
+
72
+ end
73
+
74
+
75
+ it "should work when updated as a field" do
76
+ u = User.new
77
+ u.first_name = "George"
78
+ u.save
79
+
80
+ u.first_initial.should == 'G'
81
+ end
82
+
83
+ it "should work when added in the constructor" do
84
+ u = User.create(first_name: "Wally")
85
+ u.first_initial.should == 'W'
86
+ end
87
+
88
+ it "should allow multiple sources for a computed field" do
89
+ u = User.create(first_name: "John", last_name: "Doe")
90
+ u.full_name.should == "John Doe"
91
+ end
92
+
93
+ it "should work with timestamps" do
94
+ u = User.create(first_name: "John", last_name: "Doe")
95
+ u.created_at.should_not be_nil
96
+ u.date.should_not be_nil
97
+ end
98
+
99
+ it "should raise a CyclicComputation error when there is a circular dependency" do
100
+ expect do
101
+ class ModelWithCycles
102
+ compute(:a) { |b| b * 2 }
103
+ compute(:b) { |a| a / 2 }
104
+ end
105
+ end.to raise_error(Compute::CyclicComputation)
106
+ end
107
+
108
+ it "should update the computed field if even one of the values changes" do
109
+ u = User.create(first_name: "John", last_name: "Doe")
110
+
111
+ u.first_name = "Bob"
112
+ u.save
113
+ u.full_name.should == "Bob Doe"
114
+
115
+ u.last_name = "Schmoe"
116
+ u.save
117
+ u.full_name.should == "Bob Schmoe"
118
+ end
119
+
120
+ it "should take dependencies into consideration" do
121
+ restaurant_bill = Bill.create(subtotal: 100)
122
+ restaurant_bill.tax.should == 5
123
+ restaurant_bill.tip.should == 15
124
+ restaurant_bill.total.should == 120
125
+ end
126
+
127
+ it "should update dependent fields when a field is updated" do
128
+ bill = Bill.create(subtotal: 100)
129
+
130
+ bill.update_attributes(subtotal: 200)
131
+
132
+ bill.tax.should == 10
133
+ bill.tip.should == 30
134
+ bill.total.should == 240
135
+ end
136
+
137
+ describe "recompute!" do
138
+
139
+ it "should only recompute the specified field" do
140
+ bill = Bill.create(subtotal: 100)
141
+ [:tax, :tip, :total].each { |c| bill.update_column(c, 0) }
142
+
143
+ bill.recompute!(:tax)
144
+ bill.tax.should == 5
145
+ bill.tip.should == 0
146
+ bill.total.should == 105
147
+ end
148
+
149
+ it "should work with multiple fields regardless of order" do
150
+ bill = Bill.create(subtotal: 100)
151
+ [:tax, :tip, :total].each { |c| bill.update_column(c, 0) }
152
+
153
+ bill.tax.should == 0
154
+ bill.tip.should == 0
155
+ bill.total.should == 0
156
+
157
+ bill.recompute!(:total, :tax, :tip)
158
+ bill.tax.should == 5
159
+ bill.tip.should == 15
160
+ bill.total.should == 120
161
+ end
162
+
163
+ it "should work with an array" do
164
+ bill = Bill.create(subtotal: 100)
165
+ [:tax, :tip, :total].each { |c| bill.update_column(c, 0) }
166
+
167
+ bill.recompute!([:tax, :tip])
168
+ bill.tax.should == 5
169
+ bill.tip.should == 15
170
+ end
171
+
172
+ it "should propagate all changes" do
173
+ bill = Bill.create(subtotal: 100)
174
+ bill.update_column(:subtotal, 200)
175
+
176
+ bill.recompute!
177
+ bill.tax.should == 10
178
+ bill.tip.should == 30
179
+ bill.total.should == 240
180
+ end
181
+
182
+ it "should work on multiple records for all columns" do
183
+ bill1 = Bill.create(subtotal: 100)
184
+ bill1.update_column(:tax, 0)
185
+ bill1.update_column(:tip, 0)
186
+
187
+ bill2 = Bill.create(subtotal: 200)
188
+ bill2.update_column(:tax, 0)
189
+ bill2.update_column(:tip, 0)
190
+
191
+ Bill.recompute!
192
+ bill1.reload
193
+ bill2.reload
194
+
195
+ bill1.tip.should == 15
196
+ bill2.tip.should == 30
197
+
198
+ bill1.tax.should == 5
199
+ bill2.tax.should == 10
200
+ end
201
+
202
+ it "should work on multiple records for specific columns" do
203
+ bill1 = Bill.create(subtotal: 100)
204
+ bill1.update_column(:tax, 0)
205
+ bill1.update_column(:tip, 0)
206
+
207
+ bill2 = Bill.create(subtotal: 200)
208
+ bill2.update_column(:tax, 0)
209
+ bill2.update_column(:tip, 0)
210
+
211
+ Bill.recompute!(:tip)
212
+ bill1.reload
213
+ bill2.reload
214
+
215
+ bill1.tip.should == 15
216
+ bill2.tip.should == 30
217
+
218
+ bill1.tax.should == 0
219
+ bill2.tax.should == 0
220
+ end
221
+
222
+ it "should work on an ActiveRecord relation" do
223
+ bill1 = Bill.create(subtotal: 100)
224
+ bill1.update_column(:tax, 0)
225
+ bill1.update_column(:tip, 0)
226
+
227
+ bill2 = Bill.create(subtotal: 200)
228
+ bill2.update_column(:tax, 0)
229
+ bill2.update_column(:tip, 0)
230
+
231
+ Bill.where(id: [bill1.id, bill2.id]).recompute!(:tip)
232
+ bill1.reload
233
+ bill2.reload
234
+
235
+ bill1.tip.should == 15
236
+ bill2.tip.should == 30
237
+
238
+ bill1.tax.should == 0
239
+ bill2.tax.should == 0
240
+ end
241
+
242
+ end
243
+
244
+
245
+ end
@@ -0,0 +1,9 @@
1
+ require 'compute'
2
+ require "active_record"
3
+ require 'with_model'
4
+
5
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ":memory:")
6
+
7
+ RSpec.configure do |config|
8
+ config.extend WithModel
9
+ end
metadata ADDED
@@ -0,0 +1,141 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: compute
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.4
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Venkat Dinavahi
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-29 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: sqlite3
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: debugger
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: with_model
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: ActiveRecord extension for providing computed fields.
95
+ email:
96
+ - vendiddy@gmail.com
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - .gitignore
102
+ - Gemfile
103
+ - LICENSE
104
+ - README.md
105
+ - Rakefile
106
+ - compute.gemspec
107
+ - lib/compute.rb
108
+ - lib/compute/computation.rb
109
+ - lib/compute/computation_graph.rb
110
+ - lib/compute/railtie.rb
111
+ - lib/compute/version.rb
112
+ - spec/compute/compute_spec.rb
113
+ - spec/spec_helper.rb
114
+ homepage: ''
115
+ licenses: []
116
+ post_install_message:
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: 1.9.3
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - ! '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubyforge_project:
134
+ rubygems_version: 1.8.24
135
+ signing_key:
136
+ specification_version: 3
137
+ summary: Provides a compute DSL for creating ActiveRecord columns that are computed
138
+ from other ones. Automatically keeps the columns up to date.
139
+ test_files:
140
+ - spec/compute/compute_spec.rb
141
+ - spec/spec_helper.rb