calculable_attrs 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e0b5dd4bb3612fe4d97e9abf8c246d1e2e0fc540
4
+ data.tar.gz: c1f98f2e019eff2f6caa8b2a973f432a991b1512
5
+ SHA512:
6
+ metadata.gz: 531805fd07f189896eed424f7613469fc3c7b16cd36f79997fbe7624bcec6551edd0aa6cf7430d232e755d5663a7c5f69946a6df7a1167df29128bd90e4f748f
7
+ data.tar.gz: 19a1fd7a6a42b6047a852e7bb746324708b5e733f5b1dc88049f61dc99026ae1bd3192cd8a79294931defdefec2e6664cfe97c04baff3a72dde8e431f86f832d
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2014 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'CalculableAttrs'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+ Bundler::GemHelper.install_tasks
21
+
@@ -0,0 +1,77 @@
1
+ module CalculableAttrs::ActiveRecord::Base
2
+ module ClassMethods
3
+ def calculable_attr(attrs, &block)
4
+ relation = block ? lambda(&block) : attrs.delete(:from)
5
+ raise "CALCULABLE_ATTRS: Relation was missed." unless relation
6
+
7
+ foreign_key = attrs.delete(:foreign_key) || "#{ name.tableize }.id"
8
+ raise "CALCULABLE_ATTRS: At least one calculable attribute required." if attrs.empty?
9
+
10
+ calculator = CalculableAttrs::Calculator.new(foreign_key: foreign_key, relation: relation, attributes: attrs)
11
+
12
+ attrs.keys.each do |k|
13
+ calculable_attrs_calculators[k.to_sym] = calculator
14
+ class_eval <<-ruby
15
+ def #{ k }
16
+ calculable_attr_value(:#{ k })
17
+ end
18
+ ruby
19
+ end
20
+ end
21
+
22
+ def calculable_attrs_calculators
23
+ @calculable_attrs_calculators ||= {}
24
+ end
25
+
26
+ def calculable_attrs
27
+ calculable_attrs_calculators.keys
28
+ end
29
+ end
30
+
31
+ def self.included(base)
32
+ base.extend(ClassMethods)
33
+ end
34
+
35
+ def calculable_attr_value(name)
36
+ name = name.to_sym
37
+ check_calculable_attr_name!(name)
38
+ unless calculable_attrs_calculated_flags[name]
39
+ recalc_calculable_attr_with_siblings(name)
40
+ end
41
+ calculable_attrs_values[name]
42
+ end
43
+
44
+ def set_calculable_attr_value(name, value)
45
+ name = name.to_sym
46
+ check_calculable_attr_name!(name)
47
+ calculable_attrs_calculated_flags[name] = true
48
+ calculable_attrs_values[name] = value
49
+ end
50
+
51
+ def calculable_attrs_values
52
+ @calculable_attrs_values ||= {}
53
+ end
54
+
55
+ def calculable_attrs_values=(values)
56
+ values.each { |key, value| set_calculable_attr_value(key, value) }
57
+ end
58
+
59
+ def recalc_calculable_attr_with_siblings(name)
60
+ name = name.to_sym
61
+ check_calculable_attr_name!(name)
62
+ calculator = self.class.calculable_attrs_calculators[name]
63
+ self.calculable_attrs_values = calculator.calculate_all(id)
64
+ end
65
+
66
+ private
67
+
68
+ def calculable_attrs_calculated_flags
69
+ @calculable_attrs_calculated_flags ||= {}
70
+ end
71
+
72
+ def check_calculable_attr_name!(name)
73
+ unless self.class.calculable_attrs_calculators[name.to_sym]
74
+ raise "CALCULABLE_ATTRS: Unknown calculable attribute #{ name } for model #{ self.class.name }"
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,3 @@
1
+ module CalculableAttrs::ActiveRecord::Querying
2
+ delegate :calculate_attrs, to: :all
3
+ end
@@ -0,0 +1,121 @@
1
+ module CalculableAttrs::ActiveRecord::Relation
2
+ def calculate_attrs(*attrs)
3
+ spawn.calculate_attrs!(*attrs)
4
+ end
5
+
6
+ def calculate_attrs!(*attrs)
7
+ attrs.reject!(&:blank?)
8
+ attrs.flatten!
9
+
10
+ attrs = [true] if attrs.empty?
11
+
12
+ self.calculate_attrs_values |= attrs
13
+ self
14
+ end
15
+
16
+ def calculate_attrs_values
17
+ @values[:calculate_attrs] || []
18
+ end
19
+
20
+ def calculate_attrs_values=(values)
21
+ raise ImmutableRelation if @loaded
22
+ @values[:calculate_attrs] = values
23
+ end
24
+
25
+
26
+ def self.included(base)
27
+ base.class_eval do
28
+ alias_method :calculable_orig_exec_queries, :exec_queries
29
+ def exec_queries
30
+ calculable_orig_exec_queries
31
+ append_calculable_attrs
32
+ @records
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def append_calculable_attrs
40
+ unless calculate_attrs_values.empty?
41
+ models_calculable_scopes= {}
42
+ collect_models_calculable_attrs(models_calculable_scopes, klass, calculate_attrs_values)
43
+ models_calculable_scopes = models_calculable_scopes.select { |model, scope| scope.has_attrs }
44
+ collect_models_ids(models_calculable_scopes, @records, calculate_attrs_values)
45
+ models_calculable_scopes.each { |model, sope| sope.calculate }
46
+ put_calcaulated_values(models_calculable_scopes, @records, calculate_attrs_values)
47
+ end
48
+
49
+ @records
50
+ end
51
+
52
+ def collect_models_calculable_attrs(models_calculable_scopes, klass, attrs_to_calcualte)
53
+ attrs_to_calcualte = [attrs_to_calcualte] unless attrs_to_calcualte.is_a?(Array)
54
+ scope = (models_calculable_scopes[klass] ||= CalculableAttrs::ModelCalculableAttrsScope.new(klass))
55
+ attrs_to_calcualte.each do |attrs_to_calcualte_item|
56
+
57
+ case attrs_to_calcualte_item
58
+ when Symbol
59
+ if klass.reflect_on_association(attrs_to_calcualte_item)
60
+ collect_association_calculable_attrs(models_calculable_scopes, klass, attrs_to_calcualte_item, true)
61
+ else
62
+ scope.add_attr(attrs_to_calcualte_item)
63
+ end
64
+ when true
65
+ scope.add_all_attrs
66
+ when Hash
67
+ attrs_to_calcualte_item.each do |association_name, association_attrs_to_calcualte|
68
+ collect_association_calculable_attrs(models_calculable_scopes, klass, association_name, association_attrs_to_calcualte)
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ def collect_association_calculable_attrs(models_calculable_scopes, klass, assocaition_name, association_attrs_to_calcualte)
75
+ assocaition = klass.reflect_on_association(assocaition_name)
76
+ if assocaition
77
+ collect_models_calculable_attrs(models_calculable_scopes, assocaition.klass, association_attrs_to_calcualte)
78
+ else
79
+ p "CALCUALBLE_ATTRS: WAINING: Model #{ klass.name } does't have association attribute #{ assocaition_name }."
80
+ end
81
+ end
82
+
83
+ def collect_models_ids(models_calculable_scopes, records, attrs_to_calculate)
84
+ itereate_scoped_records_recursively(models_calculable_scopes, records, attrs_to_calculate) do |scope, record|
85
+ scope.add_id(record.id)
86
+ end
87
+ end
88
+
89
+
90
+ def put_calcaulated_values(models_calculable_scopes, records, attrs_to_calculate)
91
+ itereate_scoped_records_recursively(models_calculable_scopes, records, attrs_to_calculate) do |scope, record|
92
+ record.calculable_attrs_values = scope.calcualted_attrs_values(record.id)
93
+ end
94
+ end
95
+
96
+ def itereate_scoped_records_recursively(models_calculable_scopes, records, attrs_to_calculate, &block)
97
+ itereate_records_recursively(records, attrs_to_calculate) do |record|
98
+ scope = models_calculable_scopes[record.class]
99
+ block.call(scope, record) if scope
100
+ end
101
+ end
102
+
103
+ def itereate_records_recursively(records, attrs_to_calculate, &block)
104
+ attrs_to_calcualte = [attrs_to_calcualte] unless attrs_to_calcualte.is_a?(Array)
105
+ records = [records] unless records.is_a?(Array)
106
+
107
+ records.each do |record|
108
+ block.call(record)
109
+
110
+ attrs_to_calculate.select {|item| item.is_a?(Hash)}.each do |hash|
111
+ hash.each do |assocaition_name, assocaition_attributes|
112
+ if record.respond_to?(assocaition_name)
113
+ associated_records = record.send(assocaition_name)
114
+ associated_records = associated_records.respond_to?(:to_a) ? associated_records.to_a : associated_records
115
+ itereate_records_recursively(associated_records, attrs_to_calculate, &block)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,5 @@
1
+ module CalculableAttrs::ActiveRecord; end
2
+
3
+ require 'calculable_attrs/active_record/base'
4
+ require 'calculable_attrs/active_record/relation'
5
+ require 'calculable_attrs/active_record/querying'
@@ -0,0 +1,85 @@
1
+ class CalculableAttrs::Calculator
2
+ CALCULABLE_FOREIGN_KEY = '__calculable_id__'
3
+ attr_reader :attrs
4
+
5
+ def initialize(relation: nil, foreign_key: nil, attributes: nil)
6
+ @relation = relation
7
+ @foreign_key = foreign_key
8
+
9
+ @attrs = []
10
+ @formulas = {}
11
+ @defaults = {}
12
+
13
+ attributes.each do |key, val|
14
+ key = key.to_sym
15
+ save_attribute_value(key, val)
16
+ @attrs << key
17
+ end
18
+ end
19
+
20
+ def calculate(attrs, id)
21
+ if(id.is_a?(Array))
22
+ calculate_many(attrs, id)
23
+ else
24
+ calculate_one(attrs, id)
25
+ end
26
+ end
27
+
28
+ def calculate_many(attrs, ids)
29
+ query = base_query(attrs, ids).select("#{ @foreign_key } AS #{ CALCULABLE_FOREIGN_KEY }").group(@foreign_key)
30
+ records = query.load
31
+ noramlize_many_records_result(ids, attrs, records)
32
+ end
33
+
34
+ def calculate_one(attrs, id)
35
+ record = base_query(attrs, id).load.first
36
+ noramlize_one_record_result(attrs, record)
37
+ end
38
+
39
+ def calculate_all(id)
40
+ calculate(attrs, id)
41
+ end
42
+
43
+ private
44
+
45
+ def save_attribute_value(name, value)
46
+ case value
47
+ when String
48
+ @formulas[name] = value
49
+ when Array
50
+ if value.size == 2
51
+ @formulas[name] = value[0]
52
+ @defaults[name] = value[1]
53
+ else
54
+ raise "CALCUALBEL_ATTRS: Invalid attribute array for #{ name }. Expected ['formula', default_value]"
55
+ end
56
+ else
57
+ raise "CALCUALBEL_ATTRS: Invalid attribute value for #{ name }"
58
+ end
59
+ end
60
+
61
+ def base_query(attrs, id)
62
+ @relation.call.select(build_select(attrs)).where( @foreign_key => id)
63
+ end
64
+
65
+ def noramlize_many_records_result(ids, attrs, records)
66
+ normalized = {}
67
+ records.each do |row|
68
+ id = row[CALCULABLE_FOREIGN_KEY].to_i
69
+ normalized[id] = noramlize_one_record_result(attrs, row)
70
+ end
71
+ ids.each do |id|
72
+ normalized[id] ||= noramlize_one_record_result(attrs, nil)
73
+ end
74
+ normalized
75
+ end
76
+
77
+ def noramlize_one_record_result(attrs, record)
78
+ attrs.map { |a| [a, record.try(a) || ( @defaults.key?(a) ? @defaults[a] : 0)] }.to_h
79
+ end
80
+
81
+
82
+ def build_select(attrs)
83
+ attrs.map { |a| "#{ @formulas[a] } AS #{ a }" }.join(', ')
84
+ end
85
+ end
@@ -0,0 +1,58 @@
1
+ class CalculableAttrs::ModelCalculableAttrsScope
2
+ attr_reader :model, :ids, :attrs
3
+
4
+ def initialize(model)
5
+ @model = model
6
+ @attrs = []
7
+ @ids = []
8
+ end
9
+
10
+ def add_attr(attrribute)
11
+ attrribute = attrribute.to_sym
12
+ @attrs.push(attrribute) unless @attrs.include?(attrribute)
13
+ end
14
+
15
+ def add_all_attrs
16
+ @attrs = model.calculable_attrs
17
+ end
18
+
19
+ def has_attrs
20
+ @attrs.size > 0
21
+ end
22
+
23
+ def add_id(id)
24
+ @ids.push(id.to_i)
25
+ end
26
+
27
+ def calculate
28
+ @calculable_attrs_values = nil
29
+ calculators_to_use.each do |calulator|
30
+ calcualted_values = calulator.calculate_many(attrs | @attrs, ids)
31
+ merge_calculated_values(calcualted_values)
32
+ end
33
+ end
34
+
35
+ def calcualted_attrs_values(id)
36
+ @calculable_attrs_values[id.to_i]
37
+ end
38
+
39
+
40
+ private
41
+
42
+ def merge_calculated_values(calcualted_values)
43
+ if @calculable_attrs_values
44
+ calcualted_values.each {|id,values| @calculable_attrs_values[id].merge!(calcualted_values[id]) }
45
+ else
46
+ @calculable_attrs_values = calcualted_values
47
+ end
48
+ end
49
+
50
+ def calculators_to_use
51
+ calculators_to_use = []
52
+ @attrs.each do |attribute|
53
+ calulator = @model.calculable_attrs_calculators[attribute]
54
+ calculators_to_use.push(calulator) unless calculators_to_use.include?(calulator)
55
+ end
56
+ calculators_to_use
57
+ end
58
+ end
@@ -0,0 +1,3 @@
1
+ module CalculableAttrs
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,9 @@
1
+ module Calculable; end
2
+
3
+ require 'calculable_attrs/active_record'
4
+ require 'calculable_attrs/calculator'
5
+ require 'calculable_attrs/model_calculable_attrs_scope'
6
+
7
+ ::ActiveRecord::Base.include(CalculableAttrs::ActiveRecord::Base)
8
+ ::ActiveRecord::Relation.include(CalculableAttrs::ActiveRecord::Relation)
9
+ ::ActiveRecord::Base.extend(CalculableAttrs::ActiveRecord::Querying)
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :calculable do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: calculable_attrs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Dmitry Sharkov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: factory_girl_rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '4.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '4.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: database_cleaner
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: |2
84
+ Imagine you an Account model which has many transactions.
85
+ calculable_attrs gem allows you to define Accoutn#blalace and SUM(transactions.amount) directrly in your Account model.
86
+ And solves n+1 problem for you in an elegant way.
87
+ email:
88
+ - dmitry.sharkov@gmail.com
89
+ executables: []
90
+ extensions: []
91
+ extra_rdoc_files: []
92
+ files:
93
+ - MIT-LICENSE
94
+ - Rakefile
95
+ - lib/calculable_attrs.rb
96
+ - lib/calculable_attrs/active_record.rb
97
+ - lib/calculable_attrs/active_record/base.rb
98
+ - lib/calculable_attrs/active_record/querying.rb
99
+ - lib/calculable_attrs/active_record/relation.rb
100
+ - lib/calculable_attrs/calculator.rb
101
+ - lib/calculable_attrs/model_calculable_attrs_scope.rb
102
+ - lib/calculable_attrs/version.rb
103
+ - lib/tasks/calculable_tasks.rake
104
+ homepage: https://github.com/dmitrysharkov/calculable_attrs
105
+ licenses:
106
+ - MIT
107
+ metadata: {}
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubyforge_project:
124
+ rubygems_version: 2.2.2
125
+ signing_key:
126
+ specification_version: 4
127
+ summary: Simplifies work with dynamically calculable fields.
128
+ test_files: []