flattery 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -14,7 +14,7 @@ If you are using NoSQL, you probably wouldn't design your schema in a way for wh
14
14
 
15
15
  * Ruby 1.9 or 2
16
16
  * Rails 3.x/4.x
17
- * ActiveRecord (only splite and PostgreQL tested. Others _should_ work; raise an issue if you find problems)
17
+ * ActiveRecord (only sqlite and PostgreQL tested. Others _should_ work; raise an issue if you find problems)
18
18
 
19
19
  ## Installation
20
20
 
@@ -44,7 +44,6 @@ Then just include Flattery::ValueCache in your model and define flatten_values l
44
44
 
45
45
  include Flattery::ValueCache
46
46
  flatten_value :category => :name
47
-
48
47
  end
49
48
 
50
49
  ### How to cache the value in a specific column name
@@ -57,10 +56,39 @@ If you want to store in another column name, use the :as option on the +flatten_
57
56
 
58
57
  include Flattery::ValueCache
59
58
  flatten_value :category => :name, :as => 'cat_name'
59
+ end
60
+
61
+ ### How to push updates to cached values from the source model
62
+
63
+ Given a model with a :category assoociation, and a flattery config that caches instance.category.name to instance.category_name,
64
+ you want the category_name cached value updated if the category.name changes.
65
+
66
+ This is achieved by adding the Flattery::ValueProvider to the source model and defining push_flattened_values_for like this:
67
+
68
+ class Category < ActiveRecord::Base
69
+ has_many :notes
70
+
71
+ include Flattery::ValueProvider
72
+ push_flattened_values_for :name => :notes
73
+ end
74
+
75
+ This will respect the flatten_value settings defined in that target mode (Note in this example).
76
+
77
+ ### How to push updates to cached values from the source model to a specific cache column name
78
+
79
+ If the cache column name cannot be inferred correctly, an error will be raised. Inference errors can occur if the inverse association relation cannot be determined.
60
80
 
81
+ To 'help' flattery figure out the correct column name, specify the column name with an :as option:
82
+
83
+ class Category < ActiveRecord::Base
84
+ has_many :notes
85
+
86
+ include Flattery::ValueProvider
87
+ push_flattened_values_for :name => :notes, :as => 'cat_name'
61
88
  end
62
89
 
63
90
 
91
+
64
92
  ## Contributing
65
93
 
66
94
  1. Fork it
data/lib/flattery.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require "active_support/core_ext"
2
2
  require "active_record"
3
3
  require "flattery/version"
4
+ require "flattery/exception"
4
5
  require "flattery/value_cache"
6
+ require "flattery/value_provider"
@@ -0,0 +1,12 @@
1
+ module Flattery
2
+
3
+ # A general exception
4
+ class Error < StandardError; end
5
+
6
+ # Raised when cannot successfully infer the cache column name
7
+ class CacheColumnInflectionError < Error; end
8
+
9
+ # Raised when cannot successfully iget a valid association
10
+ class InvalidAssociationError < Error; end
11
+
12
+ end
@@ -31,8 +31,8 @@ module Flattery::ValueCache
31
31
  opt = options.symbolize_keys
32
32
  as_setting = opt.delete(:as)
33
33
  association_name = opt.keys.first
34
- association_method = opt[association_name]
35
- cache_attribute = as_setting || "#{association_name}_#{association_method}"
34
+ association_method = opt[association_name].try(:to_sym)
35
+ cache_attribute = (as_setting || "#{association_name}_#{association_method}").to_s
36
36
 
37
37
  assoc = reflect_on_association(association_name)
38
38
  cache_options = if assoc && assoc.belongs_to? && assoc.klass.column_names.include?("#{association_method}")
@@ -50,8 +50,14 @@ module Flattery::ValueCache
50
50
  end
51
51
  end
52
52
 
53
+ # Returns the cache_column name given +association_name+ and +association_method+
54
+ def cache_attribute_for_association(association_name,association_method)
55
+ value_cache_options.detect{|k,v| v[:association_name] == association_name.to_sym && v[:association_method] == association_method.to_sym }.first
56
+ end
57
+
53
58
  end
54
59
 
60
+ # Command: updates cached values for related changed attributes
55
61
  def resolve_value_cache
56
62
  self.class.value_cache_options.each do |key,options|
57
63
  if changed & options[:changed_on]
@@ -61,4 +67,4 @@ module Flattery::ValueCache
61
67
  true
62
68
  end
63
69
 
64
- end
70
+ end
@@ -0,0 +1,99 @@
1
+ module Flattery::ValueProvider
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ class_attribute :value_provider_options
6
+ self.value_provider_options = {}
7
+
8
+ before_update :resolve_value_provision
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ # Command: adds flattery definition +options+.
14
+ # The +options+ define a single cache setting. To define multiple cache settings, call over again for each setting.
15
+ #
16
+ # +options+ by example:
17
+ # push_flattened_values_for :name => :notes
18
+ # # => will update the cached value of :name in all related Note model instances
19
+ # push_flattened_values_for :name => :notes, as: 'cat_name'
20
+ # # => will update the cached value of :name in the 'cat_name' column of all related Note model instances
21
+ #
22
+ # When explicitly passed nil, it clears all existing settings
23
+ #
24
+ def push_flattened_values_for(options={})
25
+ if options.nil?
26
+ self.value_provider_options = {}
27
+ return
28
+ end
29
+
30
+ self.value_provider_options ||= {}
31
+ opt = options.symbolize_keys
32
+ as_setting = opt.delete(:as)
33
+
34
+ attribute_key = opt.keys.first
35
+ association_name = opt[attribute_key]
36
+ attribute_name = "#{attribute_key}"
37
+
38
+ cached_attribute_name = (as_setting || "inflect").to_sym
39
+
40
+ assoc = reflect_on_association(association_name)
41
+ cache_options = if assoc && assoc.macro == :has_many
42
+ {
43
+ association_name: association_name,
44
+ cached_attribute_name: cached_attribute_name,
45
+ method: :update_all
46
+ }
47
+ end
48
+
49
+ if cache_options
50
+ self.value_provider_options[attribute_name] = cache_options
51
+ else
52
+ self.value_provider_options.delete(attribute_name)
53
+ end
54
+ end
55
+
56
+ end
57
+
58
+ # Command: pushes cache updates for related changed attributes
59
+ def resolve_value_provision
60
+ self.class.value_provider_options.each do |key,options|
61
+ if changed.include?(key)
62
+
63
+ association_name = options[:association_name]
64
+
65
+ cache_column = if options[:cached_attribute_name] == :inflect
66
+ name = nil
67
+ if assoc = self.class.reflect_on_association(association_name)
68
+ other_assoc_name = if assoc.inverse_of
69
+ assoc.inverse_of.name
70
+ else
71
+ end
72
+ if other_assoc_name
73
+ if assoc.klass.respond_to?(:cache_attribute_for_association)
74
+ name = assoc.klass.cache_attribute_for_association(other_assoc_name,key)
75
+ end
76
+ name ||= "#{other_assoc_name}_#{key}"
77
+ end
78
+ name = nil unless name && assoc.klass.column_names.include?(name)
79
+ end
80
+ name
81
+ else
82
+ options[:cached_attribute_name]
83
+ end
84
+
85
+ if cache_column
86
+ case options[:method]
87
+ when :update_all
88
+ new_value = self.send(key)
89
+ self.send(association_name).update_all({cache_column => new_value})
90
+ end
91
+ else
92
+ raise Flattery::CacheColumnInflectionError.new("#{self.class.name} #{key}: #{options}")
93
+ end
94
+ end
95
+ end
96
+ true
97
+ end
98
+
99
+ end
@@ -1,3 +1,3 @@
1
1
  module Flattery
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -8,8 +8,10 @@ ActiveRecord::Migration.suppress_messages do
8
8
  t.string :name;
9
9
  t.belongs_to :category;
10
10
  t.string :category_name;
11
+ t.string :cat_name;
11
12
  t.string :person_name;
12
13
  t.string :person_email;
14
+ t.string :user_email;
13
15
  end
14
16
 
15
17
  create_table(:categories, :force => true) do |t|
@@ -35,7 +37,7 @@ class Category < ActiveRecord::Base
35
37
  end
36
38
 
37
39
  class Person < ActiveRecord::Base
38
- has_many :notes, inverse_of: :person
40
+ has_many :notes, primary_key: "username", foreign_key: "person_name", inverse_of: :person
39
41
  end
40
42
 
41
43
  module ArHelper
@@ -51,4 +53,4 @@ end
51
53
 
52
54
  RSpec.configure do |conf|
53
55
  conf.include ArHelper
54
- end
56
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Flattery Exceptions" do
4
+
5
+ [
6
+ Flattery::Error,
7
+ Flattery::CacheColumnInflectionError
8
+ ].each do |exception_class|
9
+ describe exception_class do
10
+ subject { raise exception_class.new("test") }
11
+ it "should raise correctly" do
12
+ expect { subject }.to raise_error(exception_class)
13
+ end
14
+ end
15
+ end
16
+
17
+ end
@@ -23,7 +23,18 @@ describe Flattery::ValueCache do
23
23
  it { should be_empty }
24
24
  end
25
25
 
26
- context "with simple belongs_to associations" do
26
+ context "when reset with nil" do
27
+ let(:flatten_value_options) { {category: :name} }
28
+ it "should clear all settings" do
29
+ expect {
30
+ resource_class.flatten_value nil
31
+ }.to change {
32
+ resource_class.value_cache_options
33
+ }.to({})
34
+ end
35
+ end
36
+
37
+ context "with simple belongs_to association" do
27
38
 
28
39
  context "when set by association name and attribute value" do
29
40
  let(:flatten_value_options) { {category: :name} }
@@ -34,16 +45,28 @@ describe Flattery::ValueCache do
34
45
  changed_on: ["category_id"]
35
46
  }
36
47
  }) }
48
+ end
37
49
 
38
- context "when force reset to nil" do
39
- it "should clear all settings" do
40
- expect {
41
- resource_class.flatten_value nil
42
- }.to change {
43
- resource_class.value_cache_options
44
- }.to({})
45
- end
46
- end
50
+ context "when given a cache column override" do
51
+ let(:flatten_value_options) { {category: :name, as: :cat_name} }
52
+ it { should eql({
53
+ "cat_name" => {
54
+ association_name: :category,
55
+ association_method: :name,
56
+ changed_on: ["category_id"]
57
+ }
58
+ }) }
59
+ end
60
+
61
+ context "when set using Strings" do
62
+ let(:flatten_value_options) { {'category' => 'name', 'as' => 'cat_name'} }
63
+ it { should eql({
64
+ "cat_name" => {
65
+ association_name: :category,
66
+ association_method: :name,
67
+ changed_on: ["category_id"]
68
+ }
69
+ }) }
47
70
  end
48
71
 
49
72
  context "when set by association name and invalid attribute value" do
@@ -130,4 +153,4 @@ describe Flattery::ValueCache do
130
153
  end
131
154
  end
132
155
 
133
- end
156
+ end
@@ -0,0 +1,195 @@
1
+ require 'spec_helper.rb'
2
+
3
+ class FlatteryValueProviderTestHarness < Category
4
+ include Flattery::ValueProvider
5
+ end
6
+
7
+ class NoteTestHarness < Note
8
+ include Flattery::ValueCache
9
+ end
10
+
11
+ class CategoryTestHarness < Category
12
+ include Flattery::ValueProvider
13
+ end
14
+
15
+ class PersonTestHarness < Person
16
+ include Flattery::ValueProvider
17
+ has_many :harness_notes, class_name: 'NoteTestHarness', primary_key: "username", foreign_key: "person_name", inverse_of: :person
18
+ end
19
+
20
+ describe Flattery::ValueProvider do
21
+
22
+ let(:resource_class) { FlatteryValueProviderTestHarness }
23
+ after { resource_class.value_provider_options = {} }
24
+
25
+ describe "##included_modules" do
26
+ subject { resource_class.included_modules }
27
+ it { should include(Flattery::ValueProvider) }
28
+ end
29
+
30
+ describe "##value_provider_options" do
31
+ before { resource_class.push_flattened_values_for push_flattened_values_for_options }
32
+ subject { resource_class.value_provider_options }
33
+
34
+ context "when set to empty" do
35
+ let(:push_flattened_values_for_options) { {} }
36
+ it { should be_empty }
37
+ end
38
+
39
+ context "when reset with nil" do
40
+ let(:push_flattened_values_for_options) { {name: :notes} }
41
+ it "should clear all settings" do
42
+ expect {
43
+ resource_class.push_flattened_values_for nil
44
+ }.to change {
45
+ resource_class.value_provider_options
46
+ }.to({})
47
+ end
48
+ end
49
+
50
+ context "with simple has_many association" do
51
+
52
+ context "when set by association name and attribute value" do
53
+ let(:push_flattened_values_for_options) { {name: :notes} }
54
+ it { should eql({
55
+ "name" => {
56
+ association_name: :notes,
57
+ cached_attribute_name: :inflect,
58
+ method: :update_all
59
+ }
60
+ }) }
61
+ end
62
+
63
+ context "when given a cache column override" do
64
+ let(:push_flattened_values_for_options) { {name: :notes, as: :category_name} }
65
+ it { should eql({
66
+ "name" => {
67
+ association_name: :notes,
68
+ cached_attribute_name: :category_name,
69
+ method: :update_all
70
+ }
71
+ }) }
72
+ end
73
+
74
+ context "when set by association name and invalid attribute value" do
75
+ let(:push_flattened_values_for_options) { {name: :bogative} }
76
+ it { should be_empty }
77
+ end
78
+
79
+ end
80
+ end
81
+
82
+ describe "#resolve_value_provision" do
83
+ it "should not be called when record created" do
84
+ resource_class.any_instance.should_receive(:resolve_value_provision).never
85
+ resource_class.create!
86
+ end
87
+ it "should be called when record updated" do
88
+ instance = resource_class.create!
89
+ instance.should_receive(:resolve_value_provision).and_return(true)
90
+ instance.save
91
+ end
92
+ end
93
+
94
+ describe "#before_update" do
95
+
96
+ context "with provider having simple has_many association and explicit cache_column name" do
97
+ let(:provider_class) { CategoryTestHarness }
98
+ let(:cache_class) { NoteTestHarness }
99
+ before do
100
+ provider_class.push_flattened_values_for name: :notes, as: :category_name
101
+ cache_class.flatten_value category: :name
102
+ end
103
+ after do
104
+ provider_class.value_provider_options = {}
105
+ cache_class.value_cache_options = {}
106
+ end
107
+ let!(:resource) { provider_class.create(name: 'category_a') }
108
+ let!(:target_a) { cache_class.create(category_id: resource.id) }
109
+ let!(:target_other_a) { cache_class.create }
110
+ context "when cached value is updated" do
111
+ it "should push the new cache value" do
112
+ expect {
113
+ resource.update_attributes(name: 'new category name')
114
+ }.to change {
115
+ target_a.reload.category_name
116
+ }.from('category_a').to('new category name')
117
+ end
118
+ end
119
+ end
120
+
121
+ context "with provider that cannot correctly infer the cache column name" do
122
+ let(:provider_class) { CategoryTestHarness }
123
+ let(:cache_class) { NoteTestHarness }
124
+ before do
125
+ provider_class.push_flattened_values_for name: :notes
126
+ cache_class.flatten_value category: :name
127
+ end
128
+ after do
129
+ provider_class.value_provider_options = {}
130
+ cache_class.value_cache_options = {}
131
+ end
132
+ let!(:resource) { provider_class.create(name: 'category_a') }
133
+ let!(:target_a) { cache_class.create(category_id: resource.id) }
134
+ let!(:target_other_a) { cache_class.create }
135
+ context "when cached value is updated" do
136
+ it "should push the new cache value" do
137
+ expect {
138
+ resource.update_attributes(name: 'new category name')
139
+ }.to raise_error(Flattery::CacheColumnInflectionError)
140
+ end
141
+ end
142
+ end
143
+
144
+ context "with provider having has_many association with cache name inflected via inverse relation" do
145
+ let(:provider_class) { PersonTestHarness }
146
+ let(:cache_class) { NoteTestHarness }
147
+ before do
148
+ provider_class.push_flattened_values_for email: :notes
149
+ cache_class.flatten_value person: :email
150
+ end
151
+ after do
152
+ provider_class.value_provider_options = {}
153
+ cache_class.value_cache_options = {}
154
+ end
155
+ let!(:resource) { provider_class.create(username: 'user_a', email: 'email1') }
156
+ let!(:target_a) { cache_class.create(person_name: resource.username) }
157
+ let!(:target_other_a) { cache_class.create }
158
+ context "when cached value is updated" do
159
+ it "should push the new cache value" do
160
+ expect {
161
+ resource.update_attributes(email: 'email2')
162
+ }.to change {
163
+ target_a.reload.person_email
164
+ }.from('email1').to('email2')
165
+ end
166
+ end
167
+ end
168
+
169
+ context "with provider having has_many association with cache name inflected via inverse relation with custom cache column name" do
170
+ let(:provider_class) { PersonTestHarness }
171
+ let(:cache_class) { NoteTestHarness }
172
+ before do
173
+ provider_class.push_flattened_values_for email: :harness_notes
174
+ cache_class.flatten_value person: :email, as: :user_email
175
+ end
176
+ after do
177
+ provider_class.value_provider_options = {}
178
+ cache_class.value_cache_options = {}
179
+ end
180
+ let!(:resource) { provider_class.create(username: 'user_a', email: 'email1') }
181
+ let!(:target_a) { cache_class.create(person_name: resource.username) }
182
+ let!(:target_other_a) { cache_class.create }
183
+ context "when cached value is updated" do
184
+ it "should push the new cache value" do
185
+ expect {
186
+ resource.update_attributes(email: 'email2')
187
+ }.to change {
188
+ target_a.reload.user_email
189
+ }.from('email1').to('email2')
190
+ end
191
+ end
192
+ end
193
+
194
+ end
195
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flattery
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -172,11 +172,15 @@ files:
172
172
  - Rakefile
173
173
  - flattery.gemspec
174
174
  - lib/flattery.rb
175
+ - lib/flattery/exception.rb
175
176
  - lib/flattery/value_cache.rb
177
+ - lib/flattery/value_provider.rb
176
178
  - lib/flattery/version.rb
177
179
  - spec/spec_helper.rb
178
180
  - spec/support/active_record_fixtures.rb
181
+ - spec/unit/exception_spec.rb
179
182
  - spec/unit/value_cache_spec.rb
183
+ - spec/unit/value_provider_spec.rb
180
184
  homepage: https://github.com/evendis/flattery
181
185
  licenses:
182
186
  - MIT
@@ -205,4 +209,6 @@ summary: Flatter your nicely normalised AR models
205
209
  test_files:
206
210
  - spec/spec_helper.rb
207
211
  - spec/support/active_record_fixtures.rb
212
+ - spec/unit/exception_spec.rb
208
213
  - spec/unit/value_cache_spec.rb
214
+ - spec/unit/value_provider_spec.rb