flattery 0.0.1 → 0.0.2

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.
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