garner 0.3.3 → 0.4.0

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