garner 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2012 Art.sy, Frank Macreery, Daniel Doubrovkine & Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,114 @@
1
+ Garner [![Build Status](https://secure.travis-ci.org/dblock/garner.png)](http://travis-ci.org/dblock/garner)
2
+ ======
3
+
4
+ Garner is a practical Rack-based cache implementation for RESTful APIs with support for HTTP 304 Not Modified based on time and ETags, model and instance binding and hierarchical invalidation. Garner is currently targeted at [Grape](https://github.com/intridea/grape), other systems may need some work.
5
+
6
+ Usage
7
+ -----
8
+
9
+ Add Garner to Gemfile with `gem "garner"` and run `bundle install`. Include the Garner mixin into your API. Currently Grape is supported out of the box. It's also recommended to prevent clients from caching dynamic data by default using the `Garner::Middleware::Cache::Bust` middleware. See below for a detailed explanation.
10
+
11
+ ```ruby
12
+ class API < Grape::API
13
+ use Garner::Middleware::Cache::Bust
14
+ helpers Garner::Mixins::Grape::Cache
15
+ end
16
+ ```
17
+
18
+ To cache a value, invoke `cache` from within your API. Without any parameters it generates a key based on the source code location, request parameters and path, and stores the value in the cache configured as `Garner.config.cache`. The cache is automatically `Rails.cache` when mounted in Rails and an instance of `ActiveSupport::Cache::MemoryStore` otherwise.
19
+
20
+ ``` ruby
21
+ get "/" do
22
+ cache do
23
+ { :counter => 42 }
24
+ end
25
+ end
26
+ ```
27
+
28
+ To enable support for the date-based `If-Modified-Since` and the ETag-based `If-None-Match`, use `cache_or_304`. If the data hasn't changed, the API will return `304 Not Modified` without a cache miss. For example, if the inside of a cached block is a database query, it will not be executed the second time. This is possible because Garner stores an entry for every cache binding with the last-modified timestamp and ETag.
29
+
30
+ ``` ruby
31
+ get "/" do
32
+ cache_or_304 do
33
+ { :counter => 42 }
34
+ end
35
+ end
36
+ ```
37
+
38
+ The cached value can also be bound to other models. For example, if a user has an address that may or may not change when the user is saved, you will want the cached address to be invalidated every time the user record changes.
39
+
40
+ ``` ruby
41
+ get "/me/address" do
42
+ cache_or_304(:bind => [ User, current_user.id ]) do
43
+ current_user.address
44
+ end
45
+ end
46
+ ```
47
+
48
+ Binding Strategies
49
+ ------------------
50
+
51
+ The binding parameter can be an object, class, array of objects, or array of classes on which to bind the validity of the cached result contained in the subsequent block. If no bind argument is specified, the subsequent block result will remain valid until it expires due to natural causes (e.g., passage of default memcached expiry, or memcached overflow). Here are some examples of how to use the bind option.
52
+
53
+ * `bind: { klass: Widget, object: { id: params[:id] } }` will cause the subsequent block result to be invalidated on any change to the `Widget` object whose `id` attribute equals `params[:id]`.
54
+ * `bind: { klass: User, object: { id: current_user.id } }` will cause the subsequent block result to be invalidated on any change to the `User` object whose `id` attribute equals `current_user.id`. This is one way to bind a cache result to any change in the current user.
55
+ * `bind: { klass: Widget }` will cause the subsequent block result to be invalidated on any change to any object of class `Widget`. This is the appropriate strategy for index paths like `/widgets`.
56
+ * `bind: [{ klass: Widget }, { klass: User, object: { id: current_user.id } }]` will cause the subsequent block result to be invalidated on any change to either the current user, or any object of class `Widget`.
57
+
58
+ Bind supports some nice shorthands.
59
+
60
+ * `bind: [Widget]` is shorthand for `bind: { klass: Widget }`
61
+ * `bind: [Widget, params[:id]]` is shorthand for `bind: { klass: Widget, object: { id: params[:slug] } }`
62
+ * `bind: [User, { id: current_user.id }]` is shorthand for `bind: { klass: User, object: { id: current_user.id } }`
63
+ * `bind: [[Widget], [User, { id: current_user.id }]]` is shorthand for `bind: [{ klass: Widget }, { klass: User, object: { id: current_user.id } }]`
64
+
65
+ Invalidation
66
+ ------------
67
+
68
+ You must take care of data invalidation on save. Garner currently includes a mixin with support for [Mongoid](https://github.com/mongoid/mongoid). Extend `Mongoid::Document` as follows (eg. in `config/initializers/mongoid_document.rb`).
69
+
70
+ ``` ruby
71
+ module Mongoid
72
+ module Document
73
+ include Garner::Mixins::Mongoid::Document
74
+ end
75
+ end
76
+ ```
77
+
78
+ Please contribute other invalidation mixins.
79
+
80
+ Configuration
81
+ -------------
82
+
83
+ By default `Garner` will use an instance of `ActiveSupport::Cache::MemoryStore` in a non-Rails and `Rails.cache` in a Rails environment. You can configure it to use any other cache store.
84
+
85
+ ``` ruby
86
+ Garner.configure do |config|
87
+ config.cache = ActiveSupport::Cache::FileStore.new
88
+ end
89
+ ```
90
+
91
+ Preventing Clients from Caching Dynamic Data
92
+ --------------------------------------------
93
+
94
+ Generally, dynamic data cannot have a well-defined expiration time. Therefore, we must tell the client not to cache it. This can be accomplished using the `Garner::Middleware::Cache::Bust` middleware, executed after any API call. The middleware adds a `Cache-Control` and an `Expires` header.
95
+
96
+ ```
97
+ Cache-Control: private, max-age=0, must-revalidate
98
+ Expires: Fri, 01 Jan 1990 00:00:00 GMT
99
+ ```
100
+
101
+ The `private` option of the `Cache-Control` header instructs the client that it is allowed to store data in a private cache (unnecessary, but is known to work around overzealous cache implementations), `max-age` that it must check with the server every time it needs this data and `must-revalidate` prevents gateways from returning a response if your API server is unreachable. An additional `Expires` header will make double-sure the entire request expires immediately.
102
+
103
+ Contributing
104
+ ------------
105
+
106
+ Fork the project. Make your feature addition or bug fix with tests. Send a pull request. Bonus points for topic branches.
107
+
108
+ Copyright and License
109
+ ---------------------
110
+
111
+ MIT License, see [LICENSE](https://github.com/dblock/garner/blob/master/LICENSE.md) for details.
112
+
113
+ (c) 2012 [Art.sy Inc.](http://artsy.github.com), [Frank Macreery](https://github.com/macreery), [Daniel Doubrovkine](https://github.com/dblock) and [Contributors](https://github.com/dblock/garner/blob/master/CHANGELOG.md)
114
+
@@ -0,0 +1,214 @@
1
+ module Garner
2
+ module Cache
3
+ #
4
+ # A cache that uses an object identity binding strategy.
5
+ #
6
+ # Allows some flexibility in how caller binds objects in cache.
7
+ # The binding can be an object, class, array of objects, or array of classes
8
+ # on which to bind the validity of the cached result contained in the subsequent
9
+ # block.
10
+ #
11
+ # @example `bind: { klass: Widget, object: { id: params[:id] } }` will cause a cached instance to be
12
+ # invalidated on any change to the `Widget` object whose slug attribute equals `params[:id]`
13
+ #
14
+ # @example `bind: { klass: User, object: { id: current_user.id } }` will cause a cached instance to be
15
+ # invalidated on any change to the `User` object whose id attribute equals current_user.id.
16
+ # This is one way to bind a cache result to any change in the current user.
17
+ #
18
+ # @example `bind: { klass: Widget }` will cause the cached instance to be invalidated on any change to
19
+ # any object of class Widget. This is the appropriate strategy for index paths like /widgets.
20
+ #
21
+ # @example `bind: [{ klass: Widget }, { klass: User, object: { id: current_user.id } }]` will cause a
22
+ # cached instance to be invalidated on any change to either the current user, or any object of class Widget.
23
+ #
24
+ # @example `bind: [Artwork]` is shorthand for `bind: { klass: Artwork }`
25
+ #
26
+ # @example `bind: [Artwork, params[:id]]` is shorthand for `bind: { klass: Artwork, object: { id: params[:id] } }`
27
+ #
28
+ # @example `bind: [User, { id: current_user.id }] is shorthand for `bind: { klass: User, object: { id: current_user.id } }`
29
+ #
30
+ # @example `bind: [[Artwork], [User, { id: current_user.id }]]` is shorthand for
31
+ # `bind: [{ klass: Artwork }, { klass: User, object: { id: current_user.id } }]`
32
+ #
33
+ module ObjectIdentity
34
+
35
+ IDENTITY_FIELDS = [ :id ]
36
+
37
+ KEY_STRATEGIES = [
38
+ Garner::Strategies::Keys::Caller,
39
+ Garner::Strategies::Keys::Version,
40
+ Garner::Strategies::Keys::RequestPath,
41
+ Garner::Strategies::Keys::RequestGet
42
+ ]
43
+
44
+ CACHE_STRATEGIES = [
45
+ Garner::Strategies::Cache::Expiration
46
+ ]
47
+
48
+ class << self
49
+
50
+ # cache the result of an executable block
51
+ def cache(binding = nil, context = {})
52
+ # apply cache strategies
53
+ cache_options = cache_options(context)
54
+ CACHE_STRATEGIES.each do |strategy|
55
+ cache_options = strategy.apply(cache_options)
56
+ end
57
+ key = key(binding, key_context(context))
58
+ result = Garner.config.cache.fetch(key, cache_options) do
59
+ object = yield
60
+ reset_cache_metadata(key, object)
61
+ object
62
+ end
63
+ Garner.config.cache.delete(key) unless result
64
+ result
65
+ end
66
+
67
+ # invalidate an object that has been cached
68
+ def invalidate(* args)
69
+ options = index(*args)
70
+ reset_key_prefix_for(options[:klass], options[:object])
71
+ reset_key_prefix_for(options[:klass]) if options[:object]
72
+ end
73
+
74
+ # metadata for cached objects:
75
+ # :etag - Unique hash of object content
76
+ # :last_modified - Timestamp of last modification event
77
+ def cache_metadata(binding, context = {})
78
+ key = key(binding, key_context(context))
79
+ Garner.config.cache.read(meta(key))
80
+ end
81
+
82
+ private
83
+
84
+ # applied cache options
85
+ def cache_options(context)
86
+ context[:cache_options] || {}
87
+ end
88
+
89
+ # applied key context
90
+ def key_context(context)
91
+ new_context = {}
92
+ context ||= {}
93
+ KEY_STRATEGIES.each do |strategy|
94
+ new_context = strategy.apply(new_context, context)
95
+ end
96
+ new_context
97
+ end
98
+
99
+ def reset_key_prefix_for(klass, object = nil)
100
+ Garner.config.cache.write(
101
+ index_string_for(klass, object),
102
+ new_key_prefix_for(klass, object),
103
+ {}
104
+ )
105
+ end
106
+
107
+ def reset_cache_metadata(key, object)
108
+ return unless object
109
+ metadata = {
110
+ :etag => Garner::Objects::ETag.from(object),
111
+ :last_modified => Time.now
112
+ }
113
+ meta_key = meta(key)
114
+ Garner.config.cache.write(meta_key, metadata)
115
+ end
116
+
117
+ def new_key_prefix_for(klass, object = nil)
118
+ Digest::MD5.hexdigest("#{klass}/#{object || "*"}:#{new_key_postfix}")
119
+ end
120
+
121
+ # Generate a key in the Klass/id format.
122
+ # @example Widget/id=1,Gadget/slug=forty-two,Fudget/*
123
+ def key(binding = nil, context = {})
124
+ bound = binding && binding[:bind] ? standardize(binding[:bind]) : {}
125
+ bound = (bound.is_a?(Array) ? bound : [ bound ]).compact
126
+ bound.collect { |el|
127
+ if el[:object] && ! IDENTITY_FIELDS.map { |id| el[:object][id] }.compact.any?
128
+ raise ArgumentError, ":bind object arguments (#{bound}) can only be keyed by #{IDENTITY_FIELDS.join(", ")}"
129
+ end
130
+ find_or_create_key_prefix_for(el[:klass], el[:object])
131
+ }.join(",") + ":" +
132
+ Digest::MD5.hexdigest(
133
+ KEY_STRATEGIES.map { |strategy| context[strategy.field] }.compact.join("\n")
134
+ )
135
+ end
136
+
137
+ # Generate an index key from args
138
+ def index(* args)
139
+ case args[0]
140
+ when Hash
141
+ args[0]
142
+ when Class
143
+ case args[1]
144
+ when Hash
145
+ { :klass => args[0], :object => args[1] }
146
+ when NilClass
147
+ { :klass => args[0] }
148
+ else
149
+ { :klass => args[0], :object => { IDENTITY_FIELDS.first => args[1] } }
150
+ end
151
+ else
152
+ raise ArgumentError, "invalid args, must be (klass, identifier) or hash (#{args})"
153
+ end
154
+ end
155
+
156
+ def find_or_create_key_prefix_for(klass, object = nil)
157
+ Garner.config.cache.fetch(index_string_for(klass, object), {}) do
158
+ new_key_prefix_for(klass, object)
159
+ end
160
+ end
161
+
162
+ def new_key_prefix_for(klass, object = nil)
163
+ Digest::MD5.hexdigest("#{klass}/#{object || "*"}:#{new_key_postfix}")
164
+ end
165
+
166
+ def new_key_postfix
167
+ SecureRandom.respond_to?(:uuid) ? SecureRandom.uuid : (0...16).map{ ('a'..'z').to_a[rand(26)] }.join
168
+ end
169
+
170
+ def standardize(binding)
171
+ case binding
172
+ when Hash
173
+ binding
174
+ when Array
175
+ bind_array(binding)
176
+ when NilClass
177
+ nil
178
+ end
179
+ end
180
+
181
+ # Generate a metadata key.
182
+ def meta(key)
183
+ "#{key}:meta"
184
+ end
185
+
186
+ def bind_array(ary)
187
+ case ary[0]
188
+ when Array, Hash
189
+ ary.collect { |subary| standardize(subary) }
190
+ when Class
191
+ h = { :klass => ary[0] }
192
+ h.merge!({
193
+ :object => (ary[1].is_a?(Hash) ? ary[1] : { IDENTITY_FIELDS.first => ary[1] })
194
+ }) if ary[1]
195
+ h
196
+ else
197
+ raise ArgumentError, "invalid argument type #{ary[0].class} in :bind (#{ary[0]})"
198
+ end
199
+ end
200
+
201
+ def index_string_for(klass, object = nil)
202
+ prefix = "INDEX"
203
+ IDENTITY_FIELDS.each do |field|
204
+ if object && object[field]
205
+ return "#{prefix}:#{klass}/#{field}=#{object[field]}"
206
+ end
207
+ end
208
+ "#{prefix}:#{klass}/*"
209
+ end
210
+
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,101 @@
1
+ module Garner
2
+
3
+ class << self
4
+
5
+ # Set the configuration options. Best used by passing a block.
6
+ #
7
+ # @example Set up configuration options.
8
+ # Garner.configure do |config|
9
+ # config.cache = Rails.cache
10
+ # end
11
+ #
12
+ # @return [ Config ] The configuration obejct.
13
+ def configure
14
+ block_given? ? yield(Garner::Config) : Garner::Config
15
+ end
16
+ alias :config :configure
17
+ end
18
+
19
+ module Config
20
+ extend self
21
+
22
+ # Current configuration settings.
23
+ attr_accessor :settings
24
+
25
+ # Default configuration settings.
26
+ attr_accessor :defaults
27
+
28
+ @settings = {}
29
+ @defaults = {}
30
+
31
+ # Define a configuration option with a default.
32
+ #
33
+ # @example Define the option.
34
+ # Config.option(:cache, :default => nil)
35
+ #
36
+ # @param [ Symbol ] name The name of the configuration option.
37
+ # @param [ Hash ] options Extras for the option.
38
+ #
39
+ # @option options [ Object ] :default The default value.
40
+ def option(name, options = {})
41
+ defaults[name] = settings[name] = options[:default]
42
+
43
+ class_eval <<-RUBY
44
+ def #{name}
45
+ settings[#{name.inspect}]
46
+ end
47
+
48
+ def #{name}=(value)
49
+ settings[#{name.inspect}] = value
50
+ end
51
+
52
+ def #{name}?
53
+ #{name}
54
+ end
55
+ RUBY
56
+ end
57
+
58
+ # Returns the default cache store, either Rails.cache or an instance of ActiveSupport::Cache::MemoryStore.
59
+ #
60
+ # @example Get the default cache store
61
+ # config.default_cache
62
+ #
63
+ # @return [ Cache ] The default cache store instance.
64
+ def default_cache
65
+ defined?(Rails) && Rails.respond_to?(:cache) ? Rails.cache : ::ActiveSupport::Cache::MemoryStore.new
66
+ end
67
+
68
+ # Returns the cache, or defaults to Rails cache when running in Rails or an instance of ActiveSupport::Cache::MemoryStore otherwise.
69
+ #
70
+ # @example Get the cache.
71
+ # config.cache
72
+ #
73
+ # @return [ Cache ] The configured cache or a default cache instance.
74
+ def cache
75
+ settings[:cache] = default_cache unless settings.has_key?(:cache)
76
+ settings[:cache]
77
+ end
78
+
79
+ # Sets the cache to use.
80
+ #
81
+ # @example Set the cache.
82
+ # config.cache = Rails.cache
83
+ #
84
+ # @return [ Cache ] The newly set cache.
85
+ def cache=(cache)
86
+ settings[:cache] = cache
87
+ end
88
+
89
+ # Reset the configuration options to the defaults.
90
+ #
91
+ # @example Reset the configuration options.
92
+ # config.reset!
93
+ def reset!
94
+ settings.replace(defaults)
95
+ end
96
+
97
+ # Default cache expiration time.
98
+ option(:expires_in, :default => nil)
99
+ end
100
+ end
101
+
@@ -0,0 +1,47 @@
1
+ # Based on https://github.com/intridea/grape/blob/master/lib/grape/middleware/base.rb.
2
+ module Garner
3
+ module Middleware
4
+ class Base
5
+ attr_reader :app, :env, :options
6
+
7
+ # @param [Rack Application] app The standard argument for a Rack middleware.
8
+ # @param [Hash] options A hash of options, simply stored for use by subclasses.
9
+ def initialize(app, options = {})
10
+ @app = app
11
+ @options = default_options.merge(options)
12
+ end
13
+
14
+ def default_options; {} end
15
+
16
+ def call(env)
17
+ dup.call!(env)
18
+ end
19
+
20
+ def call!(env)
21
+ @env = env
22
+ before
23
+ @app_response = @app.call(@env)
24
+ after || @app_response
25
+ end
26
+
27
+ # @abstract
28
+ # Called before the application is called in the middleware lifecycle.
29
+ def before; end
30
+
31
+ # @abstract
32
+ # Called after the application is called in the middleware lifecycle.
33
+ # @return [Response, nil] a Rack SPEC response or nil to call the application afterwards.
34
+ def after; end
35
+
36
+ def request
37
+ Rack::Request.new(self.env)
38
+ end
39
+
40
+ def response
41
+ Rack::Response.new(@app_response)
42
+ end
43
+
44
+ end
45
+ end
46
+ end
47
+
@@ -0,0 +1,20 @@
1
+ module Garner
2
+ module Middleware
3
+ module Cache
4
+ # @abstract
5
+ # Add the necessary Cache-Control and Expires headers to bust client cache.
6
+ class Bust < Garner::Middleware::Base
7
+ def after
8
+ # private: ok to store API results in a private cache
9
+ # max-age: don't reuse the cached result without checking with the server (server might say 304 Not Modified)
10
+ # must-revalidate: prevent gateways from returning a response if the API server is not reachable
11
+ @app_response[1]["Cache-Control"] = "private, max-age=0, must-revalidate"
12
+ # don't reuse the cached result without checking with the server
13
+ @app_response[1]["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
14
+ @app_response
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,104 @@
1
+ module Garner
2
+ module Mixins
3
+ module Grape
4
+ #
5
+ # A cache that supports conditional GETs
6
+ #
7
+ # Borrows generously from http://themomorohoax.com/2009/01/07/using-stale-with-rails-to-return-304-not-modified
8
+ # See also RFC 2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
9
+ # for explanation of how If-Modified-Since and If-None-Match request headers are handled.
10
+ #
11
+ module Cache
12
+
13
+ def cache_enabled?
14
+ true
15
+ end
16
+
17
+ # cache a record
18
+ def cache(options = {}, &block)
19
+ unless cache_enabled?
20
+ yield
21
+ else
22
+ binding, context = cache_binding_and_context(options)
23
+ Garner::Cache::ObjectIdentity.cache(binding, context) do
24
+ yield
25
+ end
26
+ end
27
+ end
28
+
29
+ # invalidate a cache record
30
+ def invalidate(*args)
31
+ Garner::Cache::ObjectIdentity.invalidate(* args)
32
+ end
33
+
34
+ def cache_or_304(options = {}, &block)
35
+ binding, context = cache_binding_and_context(options)
36
+ # metadata written in a previous GET
37
+ metadata = Garner::Cache::ObjectIdentity.cache_metadata(binding, context)
38
+ error!("Not Modified", 304) if metadata && fresh?(metadata)
39
+ rc = cache(options, &block)
40
+ # metadata has been generated by cache
41
+ metadata = Garner::Cache::ObjectIdentity.cache_metadata(binding, context)
42
+ self.last_modified = metadata[:last_modified]
43
+ self.etag = metadata[:etag]
44
+ rc
45
+ end
46
+
47
+ private
48
+
49
+ def cache_binding_and_context(options)
50
+ cache_context = {}
51
+ cache_context.merge!(options.dup)
52
+ cache_context[:request] = request
53
+ cache_context.delete(:bind)
54
+ cache_binding = (options || {})[:bind]
55
+ cache_binding = cache_binding ? { :bind => cache_binding } : {}
56
+ [ cache_binding, cache_context ]
57
+ end
58
+
59
+ def fresh?(metadata = {})
60
+ case
61
+ when if_modified_since && if_none_match
62
+ not_modified?(metadata[:last_modified]) && etag_matches?(metadata[:etag])
63
+ when if_modified_since
64
+ not_modified?(metadata[:last_modified])
65
+ when if_none_match
66
+ etag_matches?(metadata[:etag])
67
+ else
68
+ false
69
+ end
70
+ end
71
+
72
+ def if_modified_since
73
+ if since = env["HTTP_IF_MODIFIED_SINCE"]
74
+ Time.rfc2822(since) rescue nil
75
+ end
76
+ end
77
+
78
+ def if_none_match
79
+ env["HTTP_IF_NONE_MATCH"]
80
+ end
81
+
82
+ def not_modified?(modified_at)
83
+ if_modified_since && modified_at && if_modified_since >= modified_at
84
+ end
85
+
86
+ def etag_matches?(etag)
87
+ if_none_match && if_none_match == etag
88
+ end
89
+
90
+ def last_modified=(utc_time)
91
+ return unless utc_time
92
+ header "Last-Modified", utc_time.httpdate
93
+ end
94
+
95
+ def etag=(etag)
96
+ return unless etag
97
+ header "ETag", etag
98
+ end
99
+
100
+ end
101
+ end
102
+ end
103
+ end
104
+
@@ -0,0 +1,49 @@
1
+ module Garner
2
+ module Mixins
3
+ module Mongoid
4
+ module Document
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_save :invalidate_api_cache
9
+ before_destroy :invalidate_api_cache
10
+ cattr_accessor :api_cache_class
11
+ end
12
+
13
+ # invalidate API cache
14
+ def invalidate_api_cache
15
+ self.all_embedding_documents.each { |doc| doc.invalidate_api_cache }
16
+ cache_class = self.class.api_cache_class || self.class
17
+ Garner::Cache::ObjectIdentity::IDENTITY_FIELDS.each do |identity_field|
18
+ next unless self.respond_to?(identity_field)
19
+ Garner::Cache::ObjectIdentity.invalidate(cache_class, { identity_field => self.send(identity_field) })
20
+ end
21
+ Garner::Cache::ObjectIdentity.invalidate(cache_class)
22
+ end
23
+
24
+ # navigate the parent embedding document hierarchy
25
+ def all_embedding_documents
26
+ obj = self
27
+ docs = []
28
+ while obj.metadata && obj.embedded?
29
+ break if docs.detect { |doc| doc.class == obj.class }
30
+ parent = obj.send(obj.metadata.inverse)
31
+ docs << parent
32
+ obj = parent
33
+ end
34
+ docs
35
+ end
36
+
37
+ module ClassMethods
38
+ # Including classes can call `cache_as` to specify a different class
39
+ # on which to bind API cache objects.
40
+ # @example `Admin`, which extends `User` should call `cache_as User`
41
+ def cache_as(klass)
42
+ self.api_cache_class = klass
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
@@ -0,0 +1,20 @@
1
+ module Garner
2
+ module Objects
3
+ module ETag
4
+ class << self
5
+ # @abstract
6
+ # Serialize in a way such that the ETag matches that which would
7
+ # be returned by Rack::ETag at response time.
8
+ def from(object)
9
+ serialization = case object
10
+ when nil then ""
11
+ when String then object
12
+ when Hash then object.respond_to?(:to_json) ? object.to_json : MultiJson.dump(object)
13
+ else Marshal.dump(object)
14
+ end
15
+ %("#{Digest::MD5.hexdigest(serialization)}")
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ module Garner
2
+ module Strategies
3
+ module Cache
4
+ # Injects an expires_in value from the globl configuration.
5
+ module Expiration
6
+ class << self
7
+ def apply(current, options = {})
8
+ rc = current ? current.dup : {}
9
+ rc[:expires_in] = Garner.config.expires_in if Garner.config.expires_in && ! current[:expires_in]
10
+ rc
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ module Garner
2
+ module Strategies
3
+ module Keys
4
+ # Injects the caller's location into the key.
5
+ module Caller
6
+ class << self
7
+
8
+ def field
9
+ :caller
10
+ end
11
+
12
+ def apply(key, context = {})
13
+ rc = key ? key.dup : {}
14
+ clr = nil
15
+ caller.each do |line|
16
+ split = line.split(":")
17
+ next unless split.length >= 2
18
+ path = Pathname.new(split[0]).realpath.to_s
19
+ next unless path.include?("/app/") || path.include?("/spec/")
20
+ rc[field] = "#{path}:#{split[1]}"
21
+ break
22
+ end
23
+ rc
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ module Garner
2
+ module Strategies
3
+ module Keys
4
+ # Inject the request GET parameters into the key.
5
+ module RequestGet
6
+ class << self
7
+
8
+ def field
9
+ :params
10
+ end
11
+
12
+ def apply(key, context = {})
13
+ key = key ? key.dup : {}
14
+ key[field] = context[:request].GET.dup if context && context[:request]
15
+ key
16
+ end
17
+
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ module Garner
2
+ module Strategies
3
+ module Keys
4
+ # Inject the request path into the key.
5
+ module RequestPath
6
+ class << self
7
+
8
+ def field
9
+ :path
10
+ end
11
+
12
+ def apply(key, context = {})
13
+ key = key ? key.dup : {}
14
+ key[field] = context[:request].path if context && context[:request] && context[:request].respond_to?(:path)
15
+ key
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,29 @@
1
+ module Garner
2
+ module Strategies
3
+ module Keys
4
+ # Inject the request path into the key.
5
+ module Version
6
+ class << self
7
+
8
+ def field
9
+ :version
10
+ end
11
+
12
+ def default_version
13
+ nil
14
+ end
15
+
16
+ def apply(key, context = {})
17
+ key = key ? key.dup : {}
18
+ if context && context[:version]
19
+ key[:version] = context[:version]
20
+ elsif default_version
21
+ key[:version] = default_version
22
+ end
23
+ key
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ module Garner
2
+ VERSION = '0.1.1'
3
+ end
4
+
5
+
data/lib/garner.rb ADDED
@@ -0,0 +1,22 @@
1
+ require 'multi_json'
2
+ require 'active_support'
3
+ # garner
4
+ require 'garner/version'
5
+ require 'garner/config'
6
+ # objects
7
+ require 'garner/objects/etag'
8
+ # middleware
9
+ require 'garner/middleware/base'
10
+ require 'garner/middleware/cache/bust'
11
+ # key strategies
12
+ require 'garner/strategies/keys/version_strategy'
13
+ require 'garner/strategies/keys/caller_strategy'
14
+ require 'garner/strategies/keys/request_path_strategy'
15
+ require 'garner/strategies/keys/request_get_strategy'
16
+ # cache option strategies
17
+ require 'garner/strategies/cache/expiration_strategy'
18
+ # caches
19
+ require 'garner/cache/object_identity'
20
+ # mixins
21
+ require 'garner/mixins/grape_cache'
22
+ require 'garner/mixins/mongoid_document'
metadata ADDED
@@ -0,0 +1,223 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: garner
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Daniel Doubrovkine
9
+ - Frank Macreery
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-06-22 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack
17
+ requirement: &70275893494820 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *70275893494820
26
+ - !ruby/object:Gem::Dependency
27
+ name: json
28
+ requirement: &70275893493960 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *70275893493960
37
+ - !ruby/object:Gem::Dependency
38
+ name: multi_json
39
+ requirement: &70275893493440 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ type: :runtime
46
+ prerelease: false
47
+ version_requirements: *70275893493440
48
+ - !ruby/object:Gem::Dependency
49
+ name: activesupport
50
+ requirement: &70275893492560 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: *70275893492560
59
+ - !ruby/object:Gem::Dependency
60
+ name: bundler
61
+ requirement: &70275893491640 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: *70275893491640
70
+ - !ruby/object:Gem::Dependency
71
+ name: grape
72
+ requirement: &70275893490880 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - =
76
+ - !ruby/object:Gem::Version
77
+ version: 0.2.0
78
+ type: :development
79
+ prerelease: false
80
+ version_requirements: *70275893490880
81
+ - !ruby/object:Gem::Dependency
82
+ name: rack-test
83
+ requirement: &70275893489940 !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - =
87
+ - !ruby/object:Gem::Version
88
+ version: 0.6.1
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: *70275893489940
92
+ - !ruby/object:Gem::Dependency
93
+ name: rspec
94
+ requirement: &70275893489020 !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ~>
98
+ - !ruby/object:Gem::Version
99
+ version: '2.10'
100
+ type: :development
101
+ prerelease: false
102
+ version_requirements: *70275893489020
103
+ - !ruby/object:Gem::Dependency
104
+ name: jeweler
105
+ requirement: &70275893503600 !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - =
109
+ - !ruby/object:Gem::Version
110
+ version: 1.8.3
111
+ type: :development
112
+ prerelease: false
113
+ version_requirements: *70275893503600
114
+ - !ruby/object:Gem::Dependency
115
+ name: mongoid
116
+ requirement: &70275893502040 !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ~>
120
+ - !ruby/object:Gem::Version
121
+ version: '2.4'
122
+ type: :development
123
+ prerelease: false
124
+ version_requirements: *70275893502040
125
+ - !ruby/object:Gem::Dependency
126
+ name: bson_ext
127
+ requirement: &70275893500500 !ruby/object:Gem::Requirement
128
+ none: false
129
+ requirements:
130
+ - - ~>
131
+ - !ruby/object:Gem::Version
132
+ version: '1.6'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: *70275893500500
136
+ - !ruby/object:Gem::Dependency
137
+ name: yard
138
+ requirement: &70275893499420 !ruby/object:Gem::Requirement
139
+ none: false
140
+ requirements:
141
+ - - ! '>='
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ type: :development
145
+ prerelease: false
146
+ version_requirements: *70275893499420
147
+ - !ruby/object:Gem::Dependency
148
+ name: redcarpet
149
+ requirement: &70275893498780 !ruby/object:Gem::Requirement
150
+ none: false
151
+ requirements:
152
+ - - ! '>='
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ type: :development
156
+ prerelease: false
157
+ version_requirements: *70275893498780
158
+ - !ruby/object:Gem::Dependency
159
+ name: github-markup
160
+ requirement: &70275893498280 !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ! '>='
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: *70275893498280
169
+ description: Garner is a set of Rack middleware and cache helpers that implement various
170
+ strategies.
171
+ email: dblock@dblock.org
172
+ executables: []
173
+ extensions: []
174
+ extra_rdoc_files:
175
+ - LICENSE.md
176
+ - README.md
177
+ files:
178
+ - lib/garner.rb
179
+ - lib/garner/cache/object_identity.rb
180
+ - lib/garner/config.rb
181
+ - lib/garner/middleware/base.rb
182
+ - lib/garner/middleware/cache/bust.rb
183
+ - lib/garner/mixins/grape_cache.rb
184
+ - lib/garner/mixins/mongoid_document.rb
185
+ - lib/garner/objects/etag.rb
186
+ - lib/garner/strategies/cache/expiration_strategy.rb
187
+ - lib/garner/strategies/keys/caller_strategy.rb
188
+ - lib/garner/strategies/keys/request_get_strategy.rb
189
+ - lib/garner/strategies/keys/request_path_strategy.rb
190
+ - lib/garner/strategies/keys/version_strategy.rb
191
+ - lib/garner/version.rb
192
+ - LICENSE.md
193
+ - README.md
194
+ homepage: http://github.com/dblock/garner
195
+ licenses:
196
+ - MIT
197
+ post_install_message:
198
+ rdoc_options: []
199
+ require_paths:
200
+ - lib
201
+ required_ruby_version: !ruby/object:Gem::Requirement
202
+ none: false
203
+ requirements:
204
+ - - ! '>='
205
+ - !ruby/object:Gem::Version
206
+ version: '0'
207
+ segments:
208
+ - 0
209
+ hash: 3544902871771726004
210
+ required_rubygems_version: !ruby/object:Gem::Requirement
211
+ none: false
212
+ requirements:
213
+ - - ! '>='
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ requirements: []
217
+ rubyforge_project:
218
+ rubygems_version: 1.8.10
219
+ signing_key:
220
+ specification_version: 3
221
+ summary: Garner is a set of Rack middleware and cache helpers that implement various
222
+ strategies.
223
+ test_files: []