flattery 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -32,20 +32,26 @@ Or install it yourself as:
32
32
 
33
33
  ## Usage
34
34
 
35
- ### How to define a model that has cached values from a :belongs_to association
35
+ ### How to cache values from a :belongs_to association
36
36
 
37
- Given a model with a :category assoociation, and you want to cache instance.category.name as instance.category_name.
37
+ Given a model with a :belongs_to association, you want to store a (copy/cached) value from the associated record.
38
38
 
39
- First, add a migration to add :category_name column to your table with the same type as category.name.
40
- Then just include Flattery::ValueCache in your model and define flatten_values like this:
39
+ class Category < ActiveRecord::Base
40
+ has_many :notes, :inverse_of => :category
41
+ end
41
42
 
42
43
  class Note < ActiveRecord::Base
43
- belongs_to :category
44
+ belongs_to :category, :inverse_of => :notes
44
45
 
45
46
  include Flattery::ValueCache
46
47
  flatten_value :category => :name
47
48
  end
48
49
 
50
+ In this case, when you save an instance of Note, it will store the instance.category.name value as instance.category_name.
51
+ The :category_name attribute is inferred from the relationship, and is assumed to be present in the schema.
52
+ So before you can use this, you must add a migration to add the :category_name column to the notes table (with the same type as the :name column on the Category table).
53
+
54
+
49
55
  ### How to cache the value in a specific column name
50
56
 
51
57
  In the usual case, the cache column name is inferred from the association (e.g. category_name in the example above).
@@ -58,12 +64,12 @@ If you want to store in another column name, use the :as option on the +flatten_
58
64
  flatten_value :category => :name, :as => 'cat_name'
59
65
  end
60
66
 
61
- ### How to push updates to cached values from the source model
67
+ Again, you must make sure the column is correctly defined in your schema.
62
68
 
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.
69
+ ### How to push updates to cached values from the source model
65
70
 
66
- This is achieved by adding the Flattery::ValueProvider to the source model and defining push_flattened_values_for like this:
71
+ Given the example above, we have a problem if Category records are updated - the :category_name value stored in Notes gets out of sync.
72
+ The Flattery::ValueProvider module fixes this by propagating changes accordingly.
67
73
 
68
74
  class Category < ActiveRecord::Base
69
75
  has_many :notes
@@ -72,7 +78,7 @@ This is achieved by adding the Flattery::ValueProvider to the source model and d
72
78
  push_flattened_values_for :name => :notes
73
79
  end
74
80
 
75
- This will respect the flatten_value settings defined in that target mode (Note in this example).
81
+ This will push changes to Category :name to Notes records (by inference, updating the :category_name value in Notes).
76
82
 
77
83
  ### How to push updates to cached values from the source model to a specific cache column name
78
84
 
@@ -88,7 +94,6 @@ To 'help' flattery figure out the correct column name, specify the column name w
88
94
  end
89
95
 
90
96
 
91
-
92
97
  ## Contributing
93
98
 
94
99
  1. Fork it
@@ -2,5 +2,6 @@ require "active_support/core_ext"
2
2
  require "active_record"
3
3
  require "flattery/version"
4
4
  require "flattery/exception"
5
+ require "flattery/settings"
5
6
  require "flattery/value_cache"
6
7
  require "flattery/value_provider"
@@ -0,0 +1,71 @@
1
+ class Flattery::Settings
2
+
3
+ attr_accessor :klass
4
+ attr_accessor :raw_settings
5
+ attr_accessor :resolved_settings
6
+ attr_accessor :resolved
7
+
8
+ def initialize(klass=nil)
9
+ self.klass = klass
10
+ reset!
11
+ end
12
+
13
+ def add_setting(options={})
14
+ if options.nil?
15
+ reset!
16
+ return
17
+ end
18
+ return if options.empty?
19
+ unresolved!
20
+ self.raw_settings << parse_option_setting(options)
21
+ end
22
+
23
+ # Process +options+ and return a standardised raw_setting hash
24
+ def parse_option_setting(options)
25
+ cache_options = setting_template
26
+
27
+ opt = options.symbolize_keys
28
+ as_setting = opt.delete(:as).try(:to_s)
29
+ method_setting = opt.delete(:method).try(:to_sym)
30
+
31
+ if from_entity = opt.keys.first
32
+ cache_options[:from_entity] = from_entity
33
+ cache_options[:to_entity] = opt[from_entity].try(:to_sym)
34
+ end
35
+ cache_options[:as] = as_setting
36
+ cache_options[:method] = method_setting if method_setting
37
+
38
+ cache_options
39
+ end
40
+
41
+ # Returns the basic settings template
42
+ def setting_template
43
+ {}
44
+ end
45
+
46
+ # Command: mark settings as unresolved
47
+ def unresolved!
48
+ self.resolved = false
49
+ end
50
+
51
+ # Command: clear/reset all settings
52
+ def reset!
53
+ self.raw_settings = []
54
+ self.resolved_settings = {}
55
+ self.resolved = false
56
+ end
57
+
58
+ # Returns resolved settings
59
+ def settings
60
+ unless resolved
61
+ self.resolved = resolve_settings!
62
+ end
63
+ self.resolved_settings
64
+ end
65
+
66
+ # Command: sets resolved_settings. Returns true if resolution was success (which will set the resolution status)
67
+ def resolve_settings!
68
+ true
69
+ end
70
+
71
+ end
@@ -3,9 +3,8 @@ module Flattery::ValueCache
3
3
 
4
4
  included do
5
5
  class_attribute :value_cache_options
6
- self.value_cache_options = {}
7
-
8
- before_save :resolve_value_cache
6
+ self.value_cache_options = Settings.new(self)
7
+ before_save Processor.new
9
8
  end
10
9
 
11
10
  module ClassMethods
@@ -22,49 +21,12 @@ module Flattery::ValueCache
22
21
  # When explicitly passed nil, it clears all existing settings
23
22
  #
24
23
  def flatten_value(options={})
25
- if options.nil?
26
- self.value_cache_options = {}
27
- return
28
- end
29
-
30
- self.value_cache_options ||= {}
31
- opt = options.symbolize_keys
32
- as_setting = opt.delete(:as)
33
- association_name = opt.keys.first
34
- association_method = opt[association_name].try(:to_sym)
35
- cache_attribute = (as_setting || "#{association_name}_#{association_method}").to_s
36
-
37
- assoc = reflect_on_association(association_name)
38
- cache_options = if assoc && assoc.belongs_to? && assoc.klass.column_names.include?("#{association_method}")
39
- {
40
- association_name: association_name,
41
- association_method: association_method,
42
- changed_on: [assoc.foreign_key]
43
- }
44
- end
45
-
46
- if cache_options
47
- self.value_cache_options[cache_attribute] = cache_options
48
- else
49
- self.value_cache_options.delete(cache_attribute)
50
- end
51
- end
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
24
+ self.value_cache_options.add_setting(options)
56
25
  end
57
26
 
58
27
  end
59
28
 
60
- # Command: updates cached values for related changed attributes
61
- def resolve_value_cache
62
- self.class.value_cache_options.each do |key,options|
63
- if changed & options[:changed_on]
64
- self.send("#{key}=", self.send(options[:association_name]).try(:send,options[:association_method]))
65
- end
66
- end
67
- true
68
- end
69
-
70
29
  end
30
+
31
+ require "flattery/value_cache/settings"
32
+ require "flattery/value_cache/processor"
@@ -0,0 +1,18 @@
1
+ class Flattery::ValueCache::Processor
2
+
3
+ # Command: updates cached values for related changed attributes
4
+ def before_save(record)
5
+ resolved_options!(record.class).each do |key,options|
6
+ if record.changed & options[:changed_on]
7
+ record.send("#{key}=", record.send(options[:from_entity]).try(:send,options[:to_entity]))
8
+ end
9
+ end
10
+ true
11
+ end
12
+
13
+ # Command: resolves value cache options for +klass+ if required, and returns resolved options
14
+ def resolved_options!(klass)
15
+ klass.value_cache_options.settings
16
+ end
17
+
18
+ end
@@ -0,0 +1,44 @@
1
+ class Flattery::ValueCache::Settings < Flattery::Settings
2
+
3
+ # Command: sets resolved_settings. Returns true if resolution was success (which will set the resolution status)
4
+ #
5
+ # Given raw settings: [{ from_entity: :category, to_entity: :name, as: 'cat_name' }]
6
+ # Resolved settings: { 'cat_name' => { from_entity: :category, to_entity: :name, changed_on: ['category_id'] } }
7
+ #
8
+ # In the ValueCache context:
9
+ # * +from_entity+ is the association from which the cache value is obtained
10
+ # * +to_entity+ is the column that the cache value will be stored in
11
+ # * +as+ is the column name on +from_entity+ from which the cache value is to be read
12
+ #
13
+ # Validations/transformations performed:
14
+ # * from_entity is a valid association
15
+ # * cache attribute name derived from :as or inferred from association/attribute names
16
+ # * cache attribute is a valid column
17
+ #
18
+ # If any of these fail, the setting is excluded from the resolved options.
19
+ #
20
+ def resolve_settings!
21
+ self.resolved_settings = raw_settings.each_with_object({}) do |setting,memo|
22
+ from_entity = setting[:from_entity]
23
+ to_entity = setting[:to_entity]
24
+ cache_attribute = (setting[:as] || "#{from_entity}_#{to_entity}").to_s
25
+
26
+ assoc = klass.reflect_on_association(from_entity)
27
+ cache_options = if assoc && assoc.belongs_to? && klass.column_names.include?("#{cache_attribute}")
28
+ {
29
+ from_entity: from_entity,
30
+ to_entity: to_entity,
31
+ changed_on: [assoc.foreign_key]
32
+ }
33
+ end
34
+
35
+ if cache_options
36
+ memo[cache_attribute] = cache_options
37
+ else
38
+ memo.delete(cache_attribute)
39
+ end
40
+ end
41
+ true
42
+ end
43
+
44
+ end
@@ -3,9 +3,8 @@ module Flattery::ValueProvider
3
3
 
4
4
  included do
5
5
  class_attribute :value_provider_options
6
- self.value_provider_options = {}
7
-
8
- before_update :resolve_value_provision
6
+ self.value_provider_options = Settings.new(self)
7
+ before_update Processor.new
9
8
  end
10
9
 
11
10
  module ClassMethods
@@ -22,78 +21,12 @@ module Flattery::ValueProvider
22
21
  # When explicitly passed nil, it clears all existing settings
23
22
  #
24
23
  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
24
+ self.value_provider_options.add_setting(options)
54
25
  end
55
26
 
56
27
  end
57
28
 
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
29
  end
30
+
31
+ require "flattery/value_provider/settings"
32
+ require "flattery/value_provider/processor"
@@ -0,0 +1,26 @@
1
+ class Flattery::ValueProvider::Processor
2
+
3
+ # Command: pushes cache updates for related changed attributes
4
+ def before_update(record)
5
+ resolved_options!(record.class).each do |key,options|
6
+ if record.changed.include?(key)
7
+ if cache_column = options[:as]
8
+ case options[:method]
9
+ when :update_all
10
+ new_value = record.send(key)
11
+ record.send(options[:to_entity]).update_all({cache_column => new_value})
12
+ end
13
+ else
14
+ raise Flattery::CacheColumnInflectionError.new("#{record.class.name} #{key}: #{options}")
15
+ end
16
+ end
17
+ end
18
+ true
19
+ end
20
+
21
+ # Command: resolves value provider options for +klass+ if required, and returns resolved options
22
+ def resolved_options!(klass)
23
+ klass.value_provider_options.settings
24
+ end
25
+
26
+ end
@@ -0,0 +1,74 @@
1
+ class Flattery::ValueProvider::Settings < Flattery::Settings
2
+
3
+ # Returns the basic settings template
4
+ def setting_template
5
+ {method: :update_all}
6
+ end
7
+
8
+ # Command: sets resolved_settings. Returns true if resolution was success (which will set the resolution status)
9
+ #
10
+ # Given raw settings: [{ from_entity: :name, to_entity: :notes, as: 'cat_name', method: :update_all }]
11
+ # Resolved settings: { 'name' => { to_entity: :notes, as: 'cat_name', method: :update_all } }
12
+ #
13
+ # In the ValueProvider context:
14
+ # * +from_entity+ is the column that provides the cache value
15
+ # * +to_entity+ is the association to which the cache value is pushed
16
+ # * +as+ is the column name on +to_entity+ from which the cache value is to be stored
17
+ #
18
+ # Validations/transformations performed:
19
+ # * to_entity is a valid association
20
+ #
21
+ # If any of these fail, the setting is excluded from the resolved options.
22
+ #
23
+ def resolve_settings!
24
+ self.resolved_settings = raw_settings.each_with_object({}) do |setting,memo|
25
+ from_entity = setting[:from_entity]
26
+ to_entity = setting[:to_entity]
27
+
28
+ push_method = setting[:method]
29
+ attribute_name = "#{from_entity}"
30
+
31
+ assoc = klass.reflect_on_association(to_entity)
32
+ cache_options = if assoc && assoc.macro == :has_many
33
+
34
+ cached_attribute_name = if setting[:as].present?
35
+ setting[:as].to_sym
36
+ else
37
+ name = nil
38
+ other_assoc_name = if assoc.inverse_of
39
+ assoc.inverse_of.name
40
+ else
41
+ end
42
+ if other_assoc_name
43
+ name = cache_attribute_for_association(assoc.klass,other_assoc_name,attribute_name)
44
+ name ||= "#{other_assoc_name}_#{attribute_name}"
45
+ end
46
+ name = nil unless name && assoc.klass.column_names.include?(name)
47
+ name
48
+ end
49
+
50
+ {
51
+ to_entity: to_entity,
52
+ as: cached_attribute_name,
53
+ method: push_method
54
+ }
55
+ end
56
+
57
+ if cache_options
58
+ memo[attribute_name] = cache_options
59
+ else
60
+ memo.delete(attribute_name)
61
+ end
62
+
63
+ end
64
+ true
65
+ end
66
+
67
+ # Returns the cache_column name given +association_name+ and +association_method+
68
+ def cache_attribute_for_association(klass,association_name,association_method)
69
+ if klass.respond_to?(:value_cache_options)
70
+ klass.value_cache_options.settings.detect{|k,v| v[:from_entity] == association_name.to_sym && v[:to_entity] == association_method.to_sym }.first
71
+ end
72
+ end
73
+
74
+ end
@@ -1,3 +1,3 @@
1
1
  module Flattery
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -25,4 +25,8 @@ RSpec.configure do |config|
25
25
  config.before do
26
26
  truncate_records
27
27
  end
28
+
29
+ config.after do
30
+ clear_harness_classes
31
+ end
28
32
  end
@@ -49,6 +49,11 @@ module ArHelper
49
49
  Person.delete_all
50
50
  end
51
51
 
52
+ def clear_harness_classes
53
+ Object.send(:remove_const, :ValueProviderHarness) if Object.constants.include?(:ValueProviderHarness)
54
+ Object.send(:remove_const, :ValueCacheHarness) if Object.constants.include?(:ValueCacheHarness)
55
+ end
56
+
52
57
  end
53
58
 
54
59
  RSpec.configure do |conf|
@@ -0,0 +1,123 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe Flattery::Settings do
4
+ let(:settings_class) { Flattery::Settings }
5
+ let(:settings) { settings_class.new }
6
+ subject { settings }
7
+
8
+ describe "#initialize" do
9
+ its(:klass) { should be_nil }
10
+ its(:raw_settings) { should eql([]) }
11
+ its(:resolved_settings) { should eql({}) }
12
+ its(:resolved) { should be_false}
13
+ context "when given class parameter" do
14
+ let(:klass) { String }
15
+ let(:settings) { settings_class.new(klass) }
16
+ its(:klass) { should eql(klass) }
17
+ end
18
+ end
19
+
20
+ def add_dummy_settings_values
21
+ settings.raw_settings = [1,2,3,4]
22
+ settings.resolved_settings = {a: :b}
23
+ settings.resolved = true
24
+ settings
25
+ end
26
+
27
+ describe "#reset!" do
28
+ before do
29
+ add_dummy_settings_values
30
+ settings.reset!
31
+ end
32
+ its(:raw_settings) { should eql([]) }
33
+ its(:resolved_settings) { should eql({}) }
34
+ its(:resolved) { should be_false}
35
+ end
36
+
37
+ describe "#add_setting" do
38
+
39
+ context "when given empty hash" do
40
+ before { add_dummy_settings_values }
41
+ it "should not do anything" do
42
+ expect { settings.add_setting({}) }.to_not change { settings.raw_settings }.from([1,2,3,4])
43
+ end
44
+ end
45
+
46
+ context "when given nil" do
47
+ it "should cause a reset!" do
48
+ settings.should_receive(:reset!)
49
+ settings.add_setting(nil)
50
+ end
51
+ end
52
+
53
+ context "when given options as Symbols" do
54
+ before { settings.add_setting({category: :name}) }
55
+ its(:raw_settings) { should eql([
56
+ { from_entity: :category, to_entity: :name, as: nil }
57
+ ]) }
58
+ context "and then given another definition" do
59
+ before { settings.add_setting({person: :email}) }
60
+ its(:raw_settings) { should eql([
61
+ { from_entity: :category, to_entity: :name, as: nil },
62
+ { from_entity: :person, to_entity: :email, as: nil }
63
+ ]) }
64
+ context "and then reset with nil" do
65
+ before { settings.add_setting nil }
66
+ its(:raw_settings) { should eql([]) }
67
+ end
68
+ end
69
+ end
70
+
71
+ context "when optional :as specified as Symbols" do
72
+ before { settings.add_setting({category: :name, as: :cat_name}) }
73
+ its(:raw_settings) { should eql([
74
+ { from_entity: :category, to_entity: :name, as: 'cat_name' }
75
+ ]) }
76
+ end
77
+
78
+ context "when optional :method specified as Symbols" do
79
+ before { settings.add_setting({category: :name, method: :update_all}) }
80
+ its(:raw_settings) { should eql([
81
+ { from_entity: :category, to_entity: :name, as: nil, method: :update_all }
82
+ ]) }
83
+ end
84
+
85
+ context "when given options as String" do
86
+ before { settings.add_setting({'category' => 'name'}) }
87
+ its(:raw_settings) { should eql([
88
+ { from_entity: :category, to_entity: :name, as: nil }
89
+ ]) }
90
+ end
91
+
92
+ context "when optional :as specified as String" do
93
+ before { settings.add_setting({'category' => 'name', 'as' => 'cat_name'}) }
94
+ its(:raw_settings) { should eql([
95
+ { from_entity: :category, to_entity: :name, as: 'cat_name' }
96
+ ]) }
97
+ end
98
+
99
+ end
100
+
101
+ describe "#settings" do
102
+ context "when initialially not resolved" do
103
+ it "should invoke resolve_settings!" do
104
+ settings.should_receive(:resolve_settings!)
105
+ settings.settings
106
+ end
107
+ it "should mark as resolved" do
108
+ expect { settings.settings }.to change { settings.resolved }.from(false).to(true)
109
+ end
110
+ end
111
+ context "when initialially resolved" do
112
+ before { add_dummy_settings_values }
113
+ it "should not invoke resolve_settings!" do
114
+ settings.should_receive(:resolve_settings!).never
115
+ settings.settings
116
+ end
117
+ it "should not change resolved status" do
118
+ expect { settings.settings }.to_not change { settings.resolved }.from(true)
119
+ end
120
+ end
121
+ end
122
+
123
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper.rb'
2
+
3
+ # Now test caching in a range of actual scenarios...
4
+ describe Flattery::ValueCache::Processor do
5
+
6
+ context "with simple belongs_to associations with cached values" do
7
+ let(:cache_class) do
8
+ class ::ValueCacheHarness < Note
9
+ include Flattery::ValueCache
10
+ flatten_value category: :name
11
+ end
12
+ ValueCacheHarness
13
+ end
14
+ let!(:resource) { cache_class.create }
15
+
16
+ let!(:category) { Category.create(name: 'category_a') }
17
+
18
+ context "when association is changed by id" do
19
+ it "should cache the new value" do
20
+ expect {
21
+ resource.update_attributes(category_id: category.id)
22
+ }.to change {
23
+ resource.category_name
24
+ }.from(nil).to(category.name)
25
+ end
26
+ end
27
+ context "when already set" do
28
+ before { resource.update_attributes(category_id: category.id) }
29
+ context "then set to nil" do
30
+ it "should cache the new value" do
31
+ expect {
32
+ resource.update_attributes(category_id: nil)
33
+ }.to change {
34
+ resource.category_name
35
+ }.from(category.name).to(nil)
36
+ end
37
+ end
38
+ context "and associated record is destroyed" do
39
+ before { category.destroy }
40
+ it "should not recache the value when other values updated" do
41
+ expect {
42
+ resource.update_attributes(name: 'a new name')
43
+ }.to_not change {
44
+ resource.category_name
45
+ }.from(category.name)
46
+ end
47
+ end
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe Flattery::ValueCache::Settings do
4
+ let(:settings_class) { Flattery::ValueCache::Settings }
5
+ let(:settings) { cache_class.value_cache_options }
6
+ subject { settings }
7
+
8
+ context "with a standard belongs_to association" do
9
+ let(:cache_class) do
10
+ class ::ValueCacheHarness < Note
11
+ include Flattery::ValueCache
12
+ flatten_value category: :name
13
+ end
14
+ ValueCacheHarness
15
+ end
16
+ context "before resolution" do
17
+ it { should be_a(settings_class) }
18
+ its(:raw_settings) { should eql([
19
+ {from_entity: :category, to_entity: :name, as: nil}
20
+ ]) }
21
+ its(:resolved) { should be_false }
22
+ end
23
+ context "after resolution" do
24
+ before { settings.settings }
25
+ its(:resolved) { should be_true }
26
+ its(:settings) { should eql({
27
+ "category_name"=>{from_entity: :category, to_entity: :name, changed_on: ["category_id"]}
28
+ }) }
29
+ end
30
+ end
31
+
32
+ end
@@ -1,156 +1,30 @@
1
1
  require 'spec_helper.rb'
2
2
 
3
- class FlatteryValueCacheTestHarness < Note
4
- include Flattery::ValueCache
5
- end
6
-
7
3
  describe Flattery::ValueCache do
8
4
 
9
- let(:resource_class) { FlatteryValueCacheTestHarness }
10
- after { resource_class.value_cache_options = {} }
11
-
12
- describe "##included_modules" do
13
- subject { resource_class.included_modules }
14
- it { should include(Flattery::ValueCache) }
15
- end
16
-
17
- describe "##value_cache_options" do
18
- before { resource_class.flatten_value flatten_value_options }
19
- subject { resource_class.value_cache_options }
20
-
21
- context "when set to empty" do
22
- let(:flatten_value_options) { {} }
23
- it { should be_empty }
24
- end
25
-
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
38
-
39
- context "when set by association name and attribute value" do
40
- let(:flatten_value_options) { {category: :name} }
41
- it { should eql({
42
- "category_name" => {
43
- association_name: :category,
44
- association_method: :name,
45
- changed_on: ["category_id"]
46
- }
47
- }) }
48
- end
49
-
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
- }) }
70
- end
71
-
72
- context "when set by association name and invalid attribute value" do
73
- let(:flatten_value_options) { {category: :bogative} }
74
- it { should be_empty }
75
- end
76
-
77
- end
78
-
79
- context "with a belongs_to association having non-standard primary and foreign keys" do
80
-
81
- context "when set by association name and attribute value" do
82
- let(:flatten_value_options) { {person: :email} }
83
- it { should eql({
84
- "person_email" => {
85
- association_name: :person,
86
- association_method: :email,
87
- changed_on: ["person_name"]
88
- }
89
- }) }
90
- end
91
-
92
- context "when set by association name and invalid attribute value" do
93
- let(:flatten_value_options) { {person: :bogative} }
94
- it { should be_empty }
95
- end
96
-
5
+ let(:cache_class) do
6
+ class ::ValueCacheHarness < Note
7
+ include Flattery::ValueCache
97
8
  end
9
+ ValueCacheHarness
10
+ end
98
11
 
12
+ subject { cache_class }
99
13
 
100
- end
14
+ its(:included_modules) { should include(Flattery::ValueCache) }
15
+ its(:value_cache_options) { should be_a(Flattery::ValueCache::Settings) }
101
16
 
102
- describe "#resolve_value_cache" do
17
+ describe "#before_save" do
18
+ let(:processor_class) { Flattery::ValueCache::Processor }
103
19
  it "should be called when record created" do
104
- resource_class.any_instance.should_receive(:resolve_value_cache).and_return(true)
105
- resource_class.create!
20
+ processor_class.any_instance.should_receive(:before_save).and_return(true)
21
+ subject.create!
106
22
  end
107
23
  it "should be called when record updated" do
108
- instance = resource_class.create!
109
- instance.should_receive(:resolve_value_cache).and_return(true)
24
+ instance = subject.create!
25
+ processor_class.any_instance.should_receive(:before_save).and_return(true)
110
26
  instance.save
111
27
  end
112
28
  end
113
29
 
114
- describe "#before_save" do
115
- let!(:resource) { resource_class.create }
116
-
117
- context "with simple belongs_to associations with cached values" do
118
- before { resource_class.flatten_value category: :name }
119
- let!(:category) { Category.create(name: 'category_a') }
120
-
121
- context "when association is changed by id" do
122
- it "should cache the new value" do
123
- expect {
124
- resource.update_attributes(category_id: category.id)
125
- }.to change {
126
- resource.category_name
127
- }.from(nil).to(category.name)
128
- end
129
- end
130
- context "when already set" do
131
- before { resource.update_attributes(category_id: category.id) }
132
- context "then set to nil" do
133
- it "should cache the new value" do
134
- expect {
135
- resource.update_attributes(category_id: nil)
136
- }.to change {
137
- resource.category_name
138
- }.from(category.name).to(nil)
139
- end
140
- end
141
- context "and associated record is destroyed" do
142
- before { category.destroy }
143
- it "should not recache the value when other values updated" do
144
- expect {
145
- resource.update_attributes(name: 'a new name')
146
- }.to_not change {
147
- resource.category_name
148
- }.from(category.name)
149
- end
150
- end
151
- end
152
-
153
- end
154
- end
155
-
156
30
  end
@@ -0,0 +1,130 @@
1
+ require 'spec_helper.rb'
2
+
3
+ # Test caching in a range of actual scenarios
4
+ describe Flattery::ValueProvider::Processor do
5
+
6
+ context "with provider having simple has_many association and explicit cache_column name" do
7
+ let(:provider_class) do
8
+ class ::ValueProviderHarness < Category
9
+ include Flattery::ValueProvider
10
+ push_flattened_values_for name: :notes, as: :category_name
11
+ end
12
+ ValueProviderHarness
13
+ end
14
+
15
+ let(:cache_class) do
16
+ class ::ValueCacheHarness < Note
17
+ include Flattery::ValueCache
18
+ flatten_value category: :name
19
+ end
20
+ ValueCacheHarness
21
+ end
22
+
23
+ let!(:resource) { provider_class.create(name: 'category_a') }
24
+ let!(:target_a) { cache_class.create(category_id: resource.id) }
25
+ let!(:target_other_a) { cache_class.create }
26
+ context "when cached value is updated" do
27
+ it "should push the new cache value" do
28
+ expect {
29
+ resource.update_attributes(name: 'new category name')
30
+ }.to change {
31
+ target_a.reload.category_name
32
+ }.from('category_a').to('new category name')
33
+ end
34
+ end
35
+ end
36
+
37
+ context "with provider that cannot correctly infer the cache column name" do
38
+ let(:provider_class) do
39
+ class ::ValueProviderHarness < Category
40
+ include Flattery::ValueProvider
41
+ push_flattened_values_for name: :notes
42
+ end
43
+ ValueProviderHarness
44
+ end
45
+
46
+ let(:cache_class) do
47
+ class ::ValueCacheHarness < Note
48
+ include Flattery::ValueCache
49
+ flatten_value category: :name
50
+ end
51
+ ValueCacheHarness
52
+ end
53
+
54
+ let!(:resource) { provider_class.create(name: 'category_a') }
55
+ let!(:target_a) { cache_class.create(category_id: resource.id) }
56
+ let!(:target_other_a) { cache_class.create }
57
+ context "when cached value is updated" do
58
+ it "should push the new cache value" do
59
+ expect {
60
+ resource.update_attributes(name: 'new category name')
61
+ }.to raise_error(Flattery::CacheColumnInflectionError)
62
+ end
63
+ end
64
+ end
65
+
66
+ context "with provider having has_many association with cache name inflected via inverse relation" do
67
+ let(:provider_class) do
68
+ class ::ValueProviderHarness < Person
69
+ include Flattery::ValueProvider
70
+ has_many :harness_notes, class_name: 'NoteTestHarness', primary_key: "username", foreign_key: "person_name", inverse_of: :person
71
+ push_flattened_values_for email: :notes
72
+ end
73
+ ValueProviderHarness
74
+ end
75
+
76
+ let(:cache_class) do
77
+ class ::ValueCacheHarness < Note
78
+ include Flattery::ValueCache
79
+ flatten_value person: :email
80
+ end
81
+ ValueCacheHarness
82
+ end
83
+
84
+ let!(:resource) { provider_class.create(username: 'user_a', email: 'email1') }
85
+ let!(:target_a) { cache_class.create(person_name: resource.username) }
86
+ let!(:target_other_a) { cache_class.create }
87
+ context "when cached value is updated" do
88
+ it "should push the new cache value" do
89
+ expect {
90
+ resource.update_attributes(email: 'email2')
91
+ }.to change {
92
+ target_a.reload.person_email
93
+ }.from('email1').to('email2')
94
+ end
95
+ end
96
+ end
97
+
98
+ context "with provider having has_many association with cache name inflected via inverse relation with custom cache column name" do
99
+ let(:provider_class) do
100
+ class ::ValueProviderHarness < Person
101
+ include Flattery::ValueProvider
102
+ has_many :harness_notes, class_name: 'ValueCacheHarness', primary_key: "username", foreign_key: "person_name", inverse_of: :person
103
+ push_flattened_values_for email: :harness_notes
104
+ end
105
+ ValueProviderHarness
106
+ end
107
+
108
+ let(:cache_class) do
109
+ class ::ValueCacheHarness < Note
110
+ include Flattery::ValueCache
111
+ flatten_value person: :email, as: :user_email
112
+ end
113
+ ValueCacheHarness
114
+ end
115
+
116
+ let!(:resource) { provider_class.create(username: 'user_a', email: 'email1') }
117
+ let!(:target_a) { cache_class.create(person_name: resource.username) }
118
+ let!(:target_other_a) { cache_class.create }
119
+ context "when cached value is updated" do
120
+ it "should push the new cache value" do
121
+ expect {
122
+ resource.update_attributes(email: 'email2')
123
+ }.to change {
124
+ target_a.reload.user_email
125
+ }.from('email1').to('email2')
126
+ end
127
+ end
128
+ end
129
+
130
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe Flattery::ValueProvider::Settings do
4
+ let(:settings_class) { Flattery::ValueProvider::Settings }
5
+ let(:settings) { provider_class.value_provider_options }
6
+
7
+ subject { settings }
8
+
9
+ context "with a standard has_many association" do
10
+ let(:provider_class) do
11
+ class ::ValueProviderHarness < Category
12
+ include Flattery::ValueProvider
13
+ push_flattened_values_for name: :notes, as: :category_name
14
+ end
15
+ ValueProviderHarness
16
+ end
17
+ context "before resolution" do
18
+ it { should be_a(settings_class) }
19
+ its(:raw_settings) { should eql([
20
+ {from_entity: :name, to_entity: :notes, as: 'category_name', method: :update_all}
21
+ ]) }
22
+ its(:resolved) { should be_false }
23
+ end
24
+ context "after resolution" do
25
+ before { settings.settings }
26
+ its(:resolved) { should be_true }
27
+ its(:settings) { should eql({
28
+ "name"=>{to_entity: :notes, as: :category_name, method: :update_all}
29
+ }) }
30
+ end
31
+ end
32
+
33
+
34
+ end
@@ -1,195 +1,30 @@
1
1
  require 'spec_helper.rb'
2
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
3
  describe Flattery::ValueProvider do
21
4
 
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 }
5
+ let(:provider_class) do
6
+ class ::ValueProviderHarness < Category
7
+ include Flattery::ValueProvider
37
8
  end
9
+ ValueProviderHarness
10
+ end
38
11
 
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
12
+ subject { provider_class }
78
13
 
79
- end
80
- end
14
+ its(:included_modules) { should include(Flattery::ValueProvider) }
15
+ its(:value_provider_options) { should be_a(Flattery::ValueProvider::Settings) }
81
16
 
82
- describe "#resolve_value_provision" do
17
+ describe "#before_update" do
18
+ let(:processor_class) { Flattery::ValueProvider::Processor }
83
19
  it "should not be called when record created" do
84
- resource_class.any_instance.should_receive(:resolve_value_provision).never
85
- resource_class.create!
20
+ processor_class.any_instance.should_receive(:before_update).never
21
+ provider_class.create!
86
22
  end
87
23
  it "should be called when record updated" do
88
- instance = resource_class.create!
89
- instance.should_receive(:resolve_value_provision).and_return(true)
24
+ instance = provider_class.create!
25
+ processor_class.any_instance.should_receive(:before_update).and_return(true)
90
26
  instance.save
91
27
  end
92
28
  end
93
29
 
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
30
  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.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-09-09 00:00:00.000000000 Z
12
+ date: 2013-09-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -173,13 +173,23 @@ files:
173
173
  - flattery.gemspec
174
174
  - lib/flattery.rb
175
175
  - lib/flattery/exception.rb
176
+ - lib/flattery/settings.rb
176
177
  - lib/flattery/value_cache.rb
178
+ - lib/flattery/value_cache/processor.rb
179
+ - lib/flattery/value_cache/settings.rb
177
180
  - lib/flattery/value_provider.rb
181
+ - lib/flattery/value_provider/processor.rb
182
+ - lib/flattery/value_provider/settings.rb
178
183
  - lib/flattery/version.rb
179
184
  - spec/spec_helper.rb
180
185
  - spec/support/active_record_fixtures.rb
181
186
  - spec/unit/exception_spec.rb
187
+ - spec/unit/settings_spec.rb
188
+ - spec/unit/value_cache/processor_spec.rb
189
+ - spec/unit/value_cache/settings_spec.rb
182
190
  - spec/unit/value_cache_spec.rb
191
+ - spec/unit/value_provider/processor_spec.rb
192
+ - spec/unit/value_provider/settings_spec.rb
183
193
  - spec/unit/value_provider_spec.rb
184
194
  homepage: https://github.com/evendis/flattery
185
195
  licenses:
@@ -210,5 +220,10 @@ test_files:
210
220
  - spec/spec_helper.rb
211
221
  - spec/support/active_record_fixtures.rb
212
222
  - spec/unit/exception_spec.rb
223
+ - spec/unit/settings_spec.rb
224
+ - spec/unit/value_cache/processor_spec.rb
225
+ - spec/unit/value_cache/settings_spec.rb
213
226
  - spec/unit/value_cache_spec.rb
227
+ - spec/unit/value_provider/processor_spec.rb
228
+ - spec/unit/value_provider/settings_spec.rb
214
229
  - spec/unit/value_provider_spec.rb