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