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 +30 -2
- data/lib/flattery.rb +2 -0
- data/lib/flattery/exception.rb +12 -0
- data/lib/flattery/value_cache.rb +9 -3
- data/lib/flattery/value_provider.rb +99 -0
- data/lib/flattery/version.rb +1 -1
- data/spec/support/active_record_fixtures.rb +4 -2
- data/spec/unit/exception_spec.rb +17 -0
- data/spec/unit/value_cache_spec.rb +34 -11
- data/spec/unit/value_provider_spec.rb +195 -0
- metadata +7 -1
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
|
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
@@ -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
|
data/lib/flattery/value_cache.rb
CHANGED
@@ -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
|
data/lib/flattery/version.rb
CHANGED
@@ -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 "
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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.
|
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
|