garner 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/LICENSE.md +1 -1
  2. data/README.md +116 -135
  3. data/lib/garner/cache/binding.rb +58 -0
  4. data/lib/garner/cache/context.rb +27 -0
  5. data/lib/garner/cache/identity.rb +45 -0
  6. data/lib/garner/cache.rb +41 -0
  7. data/lib/garner/config.rb +46 -15
  8. data/lib/garner/mixins/mongoid/document.rb +75 -0
  9. data/lib/garner/mixins/mongoid/identity.rb +106 -0
  10. data/lib/garner/mixins/mongoid.rb +4 -0
  11. data/lib/garner/mixins/rack.rb +45 -0
  12. data/lib/garner/strategies/binding/invalidation/base.rb +26 -0
  13. data/lib/garner/strategies/binding/invalidation/touch.rb +27 -0
  14. data/lib/garner/strategies/binding/key/base.rb +19 -0
  15. data/lib/garner/strategies/binding/key/cache_key.rb +19 -0
  16. data/lib/garner/strategies/binding/key/safe_cache_key.rb +33 -0
  17. data/lib/garner/strategies/context/key/base.rb +21 -0
  18. data/lib/garner/strategies/context/key/caller.rb +83 -0
  19. data/lib/garner/strategies/context/key/jsonp.rb +30 -0
  20. data/lib/garner/strategies/context/key/request_get.rb +30 -0
  21. data/lib/garner/strategies/context/key/request_path.rb +28 -0
  22. data/lib/garner/strategies/context/key/request_post.rb +30 -0
  23. data/lib/garner/version.rb +1 -1
  24. data/lib/garner.rb +29 -26
  25. metadata +122 -22
  26. data/lib/garner/cache/object_identity.rb +0 -249
  27. data/lib/garner/middleware/base.rb +0 -47
  28. data/lib/garner/middleware/cache/bust.rb +0 -20
  29. data/lib/garner/mixins/grape_cache.rb +0 -111
  30. data/lib/garner/mixins/mongoid_document.rb +0 -58
  31. data/lib/garner/strategies/cache/expiration_strategy.rb +0 -16
  32. data/lib/garner/strategies/etags/grape_strategy.rb +0 -32
  33. data/lib/garner/strategies/etags/marshal_strategy.rb +0 -16
  34. data/lib/garner/strategies/keys/caller_strategy.rb +0 -38
  35. data/lib/garner/strategies/keys/jsonp_strategy.rb +0 -24
  36. data/lib/garner/strategies/keys/key_strategy.rb +0 -21
  37. data/lib/garner/strategies/keys/request_get_strategy.rb +0 -24
  38. data/lib/garner/strategies/keys/request_path_strategy.rb +0 -21
  39. data/lib/garner/strategies/keys/request_post_strategy.rb +0 -24
  40. data/lib/garner/strategies/keys/version_strategy.rb +0 -29
@@ -0,0 +1,75 @@
1
+ # Set up Garner configuration parameters
2
+ Garner.config.option(:mongoid_binding_key_strategy, {
3
+ :default => Garner.config.binding_key_strategy
4
+ })
5
+
6
+ Garner.config.option(:mongoid_binding_invalidation_strategy, {
7
+ :default => Garner.config.binding_invalidation_strategy
8
+ })
9
+
10
+ module Garner
11
+ module Mixins
12
+ module Mongoid
13
+ module Document
14
+ extend ActiveSupport::Concern
15
+ include Garner::Cache::Binding
16
+
17
+ def key_strategy
18
+ Garner.config.mongoid_binding_key_strategy
19
+ end
20
+
21
+ def invalidation_strategy
22
+ Garner.config.mongoid_binding_invalidation_strategy
23
+ end
24
+
25
+ included do
26
+ extend Garner::Cache::Binding
27
+
28
+ def self.cache_key
29
+ _latest_by_updated_at.try(:cache_key)
30
+ end
31
+
32
+ def self.touch
33
+ _latest_by_updated_at.try(:touch)
34
+ end
35
+
36
+ def self.updated_at
37
+ _latest_by_updated_at.try(:updated_at)
38
+ end
39
+
40
+ def self.key_strategy
41
+ Garner.config.mongoid_binding_key_strategy
42
+ end
43
+
44
+ def self.invalidation_strategy
45
+ Garner.config.mongoid_binding_invalidation_strategy
46
+ end
47
+
48
+ def self.identify(id)
49
+ Garner::Mixins::Mongoid::Identity.from_class_and_id(self, id)
50
+ end
51
+
52
+ def self.garnered_find(id)
53
+ return nil unless (binding = identify(id))
54
+ identity = Garner::Cache::Identity.new
55
+ identity.bind(binding).key({ :source => :garnered_find }) { find(id) }
56
+ end
57
+
58
+ after_create :_garner_after_create
59
+ after_update :_garner_after_update
60
+ after_destroy :_garner_after_destroy
61
+
62
+ protected
63
+ def self._latest_by_updated_at
64
+ # Only find the latest if we can order by :updated_at
65
+ return nil unless fields["updated_at"]
66
+ only(:_id, :_type, :updated_at).order_by({
67
+ :updated_at => :desc
68
+ }).first
69
+ end
70
+ end
71
+
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,106 @@
1
+ # Set up Garner configuration parameters
2
+ Garner.config.option(:mongoid_identity_fields, {
3
+ :default => [:_id]
4
+ })
5
+
6
+ module Garner
7
+ module Mixins
8
+ module Mongoid
9
+ class Identity
10
+ include Garner::Cache::Binding
11
+
12
+ attr_accessor :document, :collection_name, :conditions
13
+
14
+ def self.from_class_and_id(klass, id)
15
+ validate_class!(klass)
16
+
17
+ self.new.tap do |identity|
18
+ identity.collection_name = klass.collection_name
19
+ identity.conditions = conditions_for(klass, id)
20
+ end
21
+ end
22
+
23
+ def initialize
24
+ @conditions = {}
25
+ end
26
+
27
+ def key_strategy
28
+ Garner.config.mongoid_binding_key_strategy
29
+ end
30
+
31
+ def invalidation_strategy
32
+ Garner.config.mongoid_binding_invalidation_strategy
33
+ end
34
+
35
+ def cache_key
36
+ # See https://github.com/mongoid/mongoid/blob/f5ba1295/lib/mongoid/document.rb#L242
37
+ if updated_at
38
+ "#{model_cache_key}/#{_id}-#{updated_at.utc.to_s(:number)}"
39
+ elsif _id
40
+ "#{model_cache_key}/#{_id}"
41
+ else
42
+ "#{model_cache_key}/new"
43
+ end
44
+ end
45
+
46
+ def model_cache_key
47
+ if _type
48
+ ActiveModel::Name.new(_type.constantize).cache_key
49
+ else
50
+ @collection_name.to_s
51
+ end
52
+ end
53
+
54
+ def collection
55
+ ::Mongoid.default_session[@collection_name]
56
+ end
57
+
58
+ def document
59
+ return @document if @document
60
+
61
+ collection.where(@conditions).select({
62
+ :_id => 1,
63
+ :_type => 1,
64
+ :updated_at => 1
65
+ }).limit(1).first
66
+ end
67
+
68
+ def _id
69
+ document["_id"] if document
70
+ end
71
+
72
+ def updated_at
73
+ document["updated_at"] if document
74
+ end
75
+
76
+ def _type
77
+ document["_type"] if document
78
+ end
79
+
80
+ private
81
+ def self.validate_class!(klass)
82
+ if !klass.include?(::Mongoid::Document)
83
+ raise "Must instantiate from a Mongoid class"
84
+ elsif klass.embedded?
85
+ raise "Cannot instantiate from an embedded document class"
86
+ end
87
+ end
88
+
89
+ def self.conditions_for(klass, id)
90
+ # multiple-ID conditions
91
+ conditions = {
92
+ "$or" => Garner.config.mongoid_identity_fields.map { |field|
93
+ { field => id }
94
+ }
95
+ }
96
+
97
+ # _type conditions
98
+ selector = klass.where({})
99
+ conditions.merge!(selector.send(:type_selection)) if selector.send(:type_selectable?)
100
+
101
+ conditions
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,4 @@
1
+ require "garner"
2
+
3
+ require "garner/mixins/mongoid/document"
4
+ require "garner/mixins/mongoid/identity"
@@ -0,0 +1,45 @@
1
+ require "garner"
2
+
3
+ # Set up Garner configuration parameters
4
+ Garner.config.option(:rack_context_key_strategies, {
5
+ :default => [
6
+ Garner::Strategies::Context::Key::Caller,
7
+ Garner::Strategies::Context::Key::RequestGet,
8
+ Garner::Strategies::Context::Key::RequestPost,
9
+ Garner::Strategies::Context::Key::RequestPath
10
+ ]
11
+ })
12
+
13
+ module Garner
14
+ module Mixins
15
+ module Rack
16
+
17
+ # Override this method to conditionally disable the cache.
18
+ #
19
+ # @return [Boolean]
20
+ def cache_enabled?
21
+ true
22
+ end
23
+
24
+ # Instantiate a context-appropriate cache identity.
25
+ #
26
+ # @example
27
+ # garner.bind(current_user) do
28
+ # { count: current_user.logins.count }
29
+ # end
30
+ # @return [Garner::Cache::Identity] The cache identity.
31
+ def garner(&block)
32
+ identity = Garner::Cache::Identity.new
33
+ Garner.config.rack_context_key_strategies.each do |strategy|
34
+ identity = strategy.apply(identity, self)
35
+ end
36
+
37
+ unless cache_enabled?
38
+ identity.options({ :force_miss => true })
39
+ end
40
+
41
+ block_given? ? identity.fetch(&block) : identity
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,26 @@
1
+ module Garner
2
+ module Strategies
3
+ module Binding
4
+ module Invalidation
5
+ class Base
6
+
7
+ # Specifies whether invalidation should happen on callbacks.
8
+ #
9
+ # @param kind [Symbol] One of :create, :update, :destroy
10
+ def self.apply_on_callback?(kind = nil)
11
+ true
12
+ end
13
+
14
+ # Force-invalidate an object binding. Used when bindings are
15
+ # explicitly invalidated, via binding.invalidate_garner_caches.
16
+ #
17
+ # @param binding [Object] The binding whose caches are to be
18
+ # invalidated.
19
+ def self.apply(binding)
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ module Garner
2
+ module Strategies
3
+ module Binding
4
+ module Invalidation
5
+ class Touch < Base
6
+
7
+ # Specifies whether invalidation should happen on callbacks.
8
+ #
9
+ # @param kind [Symbol] One of :create, :update, :destroy
10
+ def self.apply_on_callback?(kind = nil)
11
+ false
12
+ end
13
+
14
+ # Force-invalidate an object binding. Used when bindings are
15
+ # explicitly invalidated, via binding.invalidate_garner_caches.
16
+ #
17
+ # @param binding [Object] The binding whose caches are to be
18
+ # invalidated.
19
+ def self.apply(binding)
20
+ binding.touch if binding.respond_to?(:touch)
21
+ end
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ module Garner
2
+ module Strategies
3
+ module Binding
4
+ module Key
5
+ class Base
6
+
7
+ # Compute a cache key from an object binding.
8
+ #
9
+ # @param binding [Object] The object from which to compute a key.
10
+ # @return [String] A cache key string.
11
+ def self.apply(binding)
12
+ nil
13
+ end
14
+
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Garner
2
+ module Strategies
3
+ module Binding
4
+ module Key
5
+ class CacheKey < Base
6
+
7
+ # Compute a cache key from an object binding.
8
+ #
9
+ # @param binding [Object] The object from which to compute a key.
10
+ # @return [String] A cache key string.
11
+ def self.apply(binding)
12
+ binding.cache_key if binding.respond_to?(:cache_key)
13
+ end
14
+
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ module Garner
2
+ module Strategies
3
+ module Binding
4
+ module Key
5
+ class SafeCacheKey < Base
6
+ VALID_FORMAT = /^(?<model>[^\/]+)\/(?<id>.+)-(?<timestamp>[0-9]{14})$/
7
+
8
+ # Compute a cache key from an object binding. Only return a key if
9
+ # :cache_key and :updated_at are both defined and present on the
10
+ # object, and if :cache_key conforms to the ActiveModel format.
11
+ #
12
+ # If all requirements are met, append the millisecond portion of
13
+ # :updated_at to :cache_key.
14
+ #
15
+ # @param binding [Object] The object from which to compute a key.
16
+ # @return [String] A cache key string.
17
+ def self.apply(binding)
18
+ return unless binding.respond_to?(:cache_key) && binding.cache_key
19
+ return unless binding.respond_to?(:updated_at) && binding.updated_at
20
+
21
+ # Check for ActiveModel cache key format
22
+ return unless binding.cache_key =~ VALID_FORMAT
23
+
24
+ decimal_portion = binding.updated_at.utc.to_f % 1
25
+ decimal_string = sprintf("%.10f", decimal_portion).gsub(/^0/, "")
26
+ "#{binding.cache_key}#{decimal_string}"
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ module Garner
2
+ module Strategies
3
+ module Context
4
+ module Key
5
+ class Base
6
+
7
+ # Compute a hash of key-value pairs from a given ruby context,
8
+ # and apply it to a cache identity.
9
+ #
10
+ # @param identity [Garner::Cache::Identity] The cache identity.
11
+ # @param ruby_context [Object] An optional Ruby context.
12
+ # @return [Garner::Cache::Identity] The modified identity.
13
+ def self.apply(identity, ruby_context = self)
14
+ identity
15
+ end
16
+
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,83 @@
1
+ module Garner
2
+ module Strategies
3
+ module Context
4
+ module Key
5
+ class Caller < Base
6
+
7
+ def self.field
8
+ :caller
9
+ end
10
+
11
+ # Determine the most likely root path for the current Garner client
12
+ # application. If in a Rails application, Rails.root is used.
13
+ # Otherwise, the nearest ancestor directory containing a Gemfile is
14
+ # used.
15
+ #
16
+ # @see Garner::Config#caller_root=
17
+ # @see Garner::Config#default_caller_root
18
+ # @return [String] The default root path.
19
+ def self.default_root
20
+ if defined?(::Rails) && ::Rails.respond_to?(:root)
21
+ ::Rails.root
22
+ else
23
+ # Try to use the nearest ancestor directory containing a Gemfile.
24
+ requiring_caller = send(:caller).detect do |line|
25
+ !line.include?(File.join("lib", "garner"))
26
+ end
27
+ return nil unless requiring_caller
28
+
29
+ requiring_file = requiring_caller.split(":")[0]
30
+ gemfile_root(File.dirname(requiring_file))
31
+ end
32
+ end
33
+
34
+ # Injects the caller's location into the key hash.
35
+ #
36
+ # @param identity [Garner::Cache::Identity] The cache identity.
37
+ # @param ruby_context [Object] An optional Ruby context.
38
+ # @return [Garner::Cache::Identity] The modified identity.
39
+ def self.apply(identity, ruby_context = self)
40
+ value = nil
41
+
42
+ if ruby_context.send(:caller)
43
+ ruby_context.send(:caller).compact.each do |line|
44
+ parts = line.match(/(?<filename>[^:]+)\:(?<lineno>[^:]+)/)
45
+ file = (Pathname.new(parts[:filename]).realpath.to_s rescue nil)
46
+ next if file.nil? || file == ""
47
+ next if file.include?(File.join("lib", "garner"))
48
+
49
+ if (root = Garner.config.caller_root)
50
+ root += File::SEPARATOR unless root[-1] == File::SEPARATOR
51
+ next unless file =~ /^#{root}/
52
+ value = "#{file.gsub(root || "", "")}:#{parts[:lineno]}"
53
+ else
54
+ value = "#{file}:#{parts[:lineno]}"
55
+ end
56
+
57
+ break
58
+ end
59
+ end
60
+
61
+ value ? identity.key(field => value) : identity
62
+ end
63
+
64
+ private
65
+ def self.gemfile_root(path)
66
+ path = Pathname.new(path).realpath.to_s
67
+ newpath = Pathname.new(File.join(path, "..")).realpath.to_s
68
+ if newpath == path
69
+ # We've reached the filesystem root; return
70
+ return nil
71
+ elsif File.exist?(File.join(newpath, "Gemfile"))
72
+ # We've struck Gemfile gold; return current path
73
+ return newpath
74
+ else
75
+ return gemfile_root(newpath)
76
+ end
77
+ end
78
+
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,30 @@
1
+ module Garner
2
+ module Strategies
3
+ module Context
4
+ module Key
5
+ class Jsonp < Base
6
+
7
+ def self.field
8
+ :request_params
9
+ end
10
+
11
+ # Strips JSONP parameters from the key.
12
+ #
13
+ # @param identity [Garner::Cache::Identity] The cache identity.
14
+ # @param ruby_context [Object] An optional Ruby context.
15
+ # @return [Garner::Cache::Identity] The modified identity.
16
+ def self.apply(identity, ruby_context = self)
17
+ key_hash = identity.key_hash
18
+ return identity unless key_hash[field]
19
+
20
+
21
+ key_hash[field].delete("callback")
22
+ key_hash[field].delete("_")
23
+ identity
24
+ end
25
+
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ module Garner
2
+ module Strategies
3
+ module Context
4
+ module Key
5
+ class RequestGet < Base
6
+
7
+ def self.field
8
+ :request_params
9
+ end
10
+
11
+ # Injects the request GET parameters into the key hash.
12
+ #
13
+ # @param identity [Garner::Cache::Identity] The cache identity.
14
+ # @param ruby_context [Object] An optional Ruby context.
15
+ # @return [Garner::Cache::Identity] The modified identity.
16
+ def self.apply(identity, ruby_context = self)
17
+ return identity unless (ruby_context.respond_to?(:request))
18
+
19
+ request = ruby_context.request
20
+ if request && ["GET", "HEAD"].include?(request.request_method)
21
+ identity.key(field => request.GET.dup)
22
+ end
23
+ identity
24
+ end
25
+
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ module Garner
2
+ module Strategies
3
+ module Context
4
+ module Key
5
+ class RequestPath < Base
6
+
7
+ def self.field
8
+ :request_path
9
+ end
10
+
11
+ # Injects the request path into the key hash.
12
+ #
13
+ # @param identity [Garner::Cache::Identity] The cache identity.
14
+ # @param ruby_context [Object] An optional Ruby context.
15
+ # @return [Garner::Cache::Identity] The modified identity.
16
+ def self.apply(identity, ruby_context = self)
17
+ return identity unless (ruby_context.respond_to?(:request))
18
+
19
+ request = ruby_context.request
20
+ identity.key(field => request.path) if request.respond_to?(:path)
21
+ identity
22
+ end
23
+
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ module Garner
2
+ module Strategies
3
+ module Context
4
+ module Key
5
+ class RequestPost < Base
6
+
7
+ def self.field
8
+ :request_params
9
+ end
10
+
11
+ # Injects the request POST parameters into the key hash.
12
+ #
13
+ # @param identity [Garner::Cache::Identity] The cache identity.
14
+ # @param ruby_context [Object] An optional Ruby context.
15
+ # @return [Garner::Cache::Identity] The modified identity.
16
+ def self.apply(identity, ruby_context = self)
17
+ return identity unless (ruby_context.respond_to?(:request))
18
+
19
+ request = ruby_context.request
20
+ if request.request_method == "POST"
21
+ identity = identity.key(field => request.POST.dup)
22
+ end
23
+ identity
24
+ end
25
+
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,3 +1,3 @@
1
1
  module Garner
2
- VERSION = '0.3.3'
2
+ VERSION = "0.4.0"
3
3
  end
data/lib/garner.rb CHANGED
@@ -1,26 +1,29 @@
1
- require 'multi_json'
2
- require 'active_support'
3
- # garner
4
- require 'garner/version'
5
- require 'garner/config'
6
- # middleware
7
- require 'garner/middleware/base'
8
- require 'garner/middleware/cache/bust'
9
- # key strategies
10
- require 'garner/strategies/keys/version_strategy'
11
- require 'garner/strategies/keys/caller_strategy'
12
- require 'garner/strategies/keys/request_path_strategy'
13
- require 'garner/strategies/keys/request_get_strategy'
14
- require 'garner/strategies/keys/request_post_strategy'
15
- require 'garner/strategies/keys/key_strategy'
16
- require 'garner/strategies/keys/jsonp_strategy'
17
- # etag strategies
18
- require 'garner/strategies/etags/grape_strategy'
19
- require 'garner/strategies/etags/marshal_strategy'
20
- # cache option strategies
21
- require 'garner/strategies/cache/expiration_strategy'
22
- # caches
23
- require 'garner/cache/object_identity'
24
- # mixins
25
- require 'garner/mixins/grape_cache' if defined?(Grape)
26
- require 'garner/mixins/mongoid_document' if defined?(Mongoid)
1
+ require "multi_json"
2
+ require "active_support"
3
+
4
+ # Garner core
5
+ require "garner/version"
6
+ require "garner/config"
7
+
8
+ # Key strategies
9
+ require "garner/strategies/context/key/base"
10
+ require "garner/strategies/context/key/caller"
11
+ require "garner/strategies/context/key/request_path"
12
+ require "garner/strategies/context/key/request_get"
13
+ require "garner/strategies/context/key/request_post"
14
+ require "garner/strategies/context/key/jsonp"
15
+
16
+ # Binding strategies
17
+ require "garner/strategies/binding/key/base"
18
+ require "garner/strategies/binding/key/cache_key"
19
+ require "garner/strategies/binding/key/safe_cache_key"
20
+
21
+ # Invalidation strategies
22
+ require "garner/strategies/binding/invalidation/base"
23
+ require "garner/strategies/binding/invalidation/touch"
24
+
25
+ # Cache
26
+ require "garner/cache"
27
+ require "garner/cache/identity"
28
+ require "garner/cache/context"
29
+ require "garner/cache/binding"