cacheable 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dcf3283fac4f544317c0b3db775ef6a55686b12f
4
+ data.tar.gz: 9d214b09f7c242a7879343034b5a9edeedc85bee
5
+ SHA512:
6
+ metadata.gz: b0065e91bae9e147ac4c134ecc7b862d576b3e4968fdf13f4360ee81a4c804608b3e0f3148164a764b2cd8d6ab0bc323ef93c412a9410ba3f6ecb015d881dbc0
7
+ data.tar.gz: a671db83562e5655161a7e92efc41f99ca7f597d0987e869dc64f6a3500e4c89582d4f545c648b77a4d1915b143432fec6db7a8c07b1a9bc1da0835174218099
@@ -0,0 +1,218 @@
1
+ # Cacheable
2
+
3
+ By [Splitwise](https://www.splitwise.com)
4
+
5
+ Cacheable is a gem which intends to adds method caching in an [aspect-oriented programming (AOP)](https://en.wikipedia.org/wiki/Aspect-oriented_programming) fashion in Ruby. Its core goals are:
6
+
7
+ * ease of use (method annotation)
8
+ * flexibility (simple adaptablability for any cache backend)
9
+ * portability (plain Ruby for use with any framework)
10
+
11
+ While Rails is not a requirement, Cacheable was built inside a mature Rails app and later extracted. This first release will seemlyless work in Rails and only includes an adapter for an in-memory cache backed by a simple Hash. This may be enough for you needs but it is more likely that additional cache adapters will need to be written.
12
+
13
+ See more about [Cache Adapters](cache-adapters.md).
14
+
15
+ ## Getting Started
16
+
17
+ Add it to your Gemfile:
18
+
19
+ ```ruby
20
+ gem 'cacheable'
21
+ ```
22
+
23
+ Set your cache adapter
24
+
25
+ ```ruby
26
+ # If you're in a Rails app place the following in config/initializers/cacheable.rb
27
+ Cacheable.cache_adapter = Rails.cache
28
+
29
+ # Otherwise you can specify the name of the adapter anywhere before you use it
30
+ Cacheable.cache_adapter = :memory
31
+ ```
32
+
33
+ ### Simple Implementation Example
34
+
35
+ Cacheable is designed to work seemlessly with your already existings codebase. Consider the following contrived class:
36
+
37
+ ```ruby
38
+ class SimpleExample
39
+ def expensive_calculation
40
+ puts 'beginning expensive method'
41
+
42
+ return 'my_result'
43
+ end
44
+ end
45
+ ```
46
+
47
+ To cache this method and it's result, simply add the following:
48
+
49
+ ```ruby
50
+ require 'cacheable' # this may not be necessary depending on your autoloading system
51
+
52
+ class SimpleExample
53
+ include Cacheable
54
+
55
+ cacheable :expensive_calculation
56
+
57
+ def expensive_calculation
58
+ puts 'beginning expensive method'
59
+
60
+ return 'my_result'
61
+ end
62
+ end
63
+ ```
64
+
65
+ **Thats it!** There's some complex Ruby magic going on under the hood but to the end user you can simply call `expensive_calculation` and the result will be retreived from the cache, if available, or generated and placed into the cache. To confirm it is working, fire up an IRB console try the following:
66
+
67
+ ```irb
68
+ > s = SimpleExample.new
69
+ > s.expensive_calculation
70
+ beginning expensive method
71
+ => "my_result"
72
+ > s.expensive_calculation
73
+ => "my_result"
74
+
75
+ # Notice that the `puts` was not output the 2nd time the method was invoked.
76
+ ```
77
+
78
+ ### Additional Methods
79
+
80
+ Cacheable also adds two useful methods to your class.
81
+
82
+ #### Skip the Cache via `#{method}_without_cache`
83
+
84
+ The cache can intentionally be skipped by appending `_without_cache` to the method name. This invocation with neither check the cache nor populate it as if you called the original method and never used Cacheable.
85
+
86
+ ```irb
87
+ > s = SimpleExample.new
88
+ > s.expensive_calculation_without_cache
89
+ beginning expensive method
90
+ => "my_result"
91
+ > s.expensive_calculation_without_cache
92
+ beginning expensive method
93
+ => "my_result"
94
+ ```
95
+
96
+ #### Remove the Value via `clear_#{method}_cache`
97
+
98
+ The cache can be cleared at any time by calling `clear_#{your_method_name}_cache`.
99
+
100
+ ```irb
101
+ > s = SimpleExample.new
102
+ > s.expensive_calculation
103
+ beginning expensive method
104
+ => "my_result"
105
+ > s.expensive_calculation
106
+ => "my_result"
107
+
108
+ > s.clear_expensive_calculation_cache
109
+ => true
110
+ > s.expensive_calculation
111
+ beginning expensive method
112
+ => "my_result"
113
+ ```
114
+
115
+ ## Additional Configuration
116
+
117
+ ### Cache Invalidation
118
+
119
+ #### Default
120
+
121
+ One of the hardest things to do correctly is cache invalidation. Cacheable handles this in a variety of ways. By default Cacheable will construct key a key in the format `[cache_key || class_name, method_name]`.
122
+
123
+ If the object responds to `cache_key` its return value will be the first element in the array. `ActiveRecord` provides [`cache_key`](https://api.rubyonrails.org/classes/ActiveRecord/Integration.html#method-i-cache_key) but it can be added to any Ruby object or overwritten. If the object does not respond to it, the name of the class will be used instead. The second element will be the name of the method as a symbol.
124
+
125
+ It is up to the cache adapter what to do with this array. For example, Rails will turn `[SomeClass, :some_method]` into `"SomeClass/some_method"`. For more information see the documentation on [Cache Adapters](cache-adapters.md)
126
+
127
+ #### Set Your Own
128
+
129
+ If (re)defining `cache_key` does not provide enough flexibility you can pass a proc to the `key_format:` option of `cacheable`.
130
+
131
+ ```ruby
132
+ class CustomKeyExample
133
+ include Cacheable
134
+
135
+ cacheable :my_method, key_format: -> (target, method_name, method_args) do
136
+ args = method_args.collect { |argument| "#{argument.class}::#{argument}" }.join
137
+ "#{method_name} called on #{target} with #{args}"
138
+ end
139
+
140
+ def my_method(arg1)
141
+
142
+ end
143
+ end
144
+ ```
145
+
146
+ * `target` is the object the method is being called on (`#<CustomKeyExample:0x0…0>`)
147
+ * `method_name` is the name of the method being cached (`:my_method`)
148
+ * `method_args` is an array of arguments being passed to the method (`[arg1]`)
149
+
150
+ So if we called `CustomKeyExample.new.my_method(123)` we would get the cache key
151
+
152
+ `"my_method called on #<CustomKeyExample:0x0…0> with Integer::123"`.
153
+
154
+ ### Conditional Cacheing
155
+
156
+ You can control if a method should be cached by supplying a proc to the `unless:` option which will get the same arguments as `key_format:`. Alternatively this method can be defined on the class and a symbol of the name of the method can be passed. **Note**: When using a symbol, the first argument will not be passed but will be available in the method as `self`. The following example will not cache the value if the first argument to the method is `false`.
157
+
158
+
159
+ ```ruby
160
+ class ConditionalCachingExample
161
+ include Cacheable
162
+
163
+ cacheable :maybe_cache, unless: :should_not_cache?
164
+
165
+ def maybe_cache(cache)
166
+
167
+ end
168
+
169
+ def should_not_cache?(_method_name, method_args)
170
+ method_args.first == false
171
+ end
172
+ end
173
+ ```
174
+
175
+ ### Cache Options
176
+
177
+ If your cache backend supports options you can pass them as the `cache_options:` option. This will be passed though untouched to the cache's `fetch` method.
178
+
179
+ ```ruby
180
+ cacheable :with_options, cache_options: {expires_in: 3_600}
181
+ ```
182
+
183
+ ### Flexible Options
184
+
185
+ You can use the same options with multiple cache methods or limit them only to specific methods:
186
+
187
+ ```
188
+ cacheable :these, :methods, :share, :options, key_format: key_proc, unless: unless_proc
189
+ cacheable :this_method_has_its_own_options, unless: unless_proc2
190
+ ```
191
+
192
+ ### Class Method Cacheing
193
+
194
+ You can cache class methods just as easily as a Ruby class is just an instance of `Class`. You simply need to `include Cacheable` within the `class << self` block. Methods can be defined in this block or outside using the `def self.` syntax.
195
+
196
+ ```ruby
197
+ class StaticMethodExample
198
+ class << self
199
+ include Cacheable
200
+
201
+ cacheable :class_method, :self_class_method
202
+
203
+ def class_method
204
+ puts 'class_method called'
205
+ end
206
+ end
207
+
208
+ def self.self_class_method
209
+ puts 'self_class_method called'
210
+ end
211
+ end
212
+ ```
213
+
214
+ ### Contributors (alphabetical by last name)
215
+
216
+ * [Jess Hottenstein](https://github.com/jhottenstein)
217
+ * [Ryan Laughlin](https://github.com/rofreg)
218
+ * [Aaron Rosenberg](https://github.com/agrberg)
@@ -0,0 +1,28 @@
1
+ # Cache Adapters
2
+
3
+ A cache adapter is an object that Cacheable can use as an interface to your system's cache. Cacheable will work out of the box using the object returned by `Rails.cache` as a cache adapter.
4
+
5
+ The other adapter provided with the library is the [Memory Adapter](lib/cacheable/cache_adapters/memory_adapter.rb). Is a simple memoizing cache used in testing. It is little more than an object that conforms to the protocol and is backed by a Ruby Hash. When writting a new cache adapter it can be used as a template.
6
+
7
+ ### Protocol
8
+
9
+ There are only two methods the cache adapter protocol requires.
10
+
11
+ #### `fetch(key, cache_options) { block }`
12
+
13
+ `fetch` takes a key and options for the cache implementation. If the key is found in the cache the associated value will be returned. If it is not found, the block will be run and the result of the block will be returned and placed in the cache.
14
+
15
+ **Note**: Unless manually defined by [setting your own key format proc](README.md#set-your-own), `key` will be an *Array*. It is the cache adapter's responsibility to turn this into whatever value your cache backend requires for keys.
16
+
17
+ #### `delete(key)`
18
+
19
+ `delete` takes a key and removes it's associated value in the cache. While not currently dependend on by Cacheable, it appears the standard is to return `true` if the value was present and removed and `false` if not present to begin with.
20
+
21
+ #### Additional useful methods
22
+
23
+ These are additional methods that are very useful to have on a cache adapter but are not depended on by Cacheable. They can be found in the Memory Adapter but they are only used to aid in testing.
24
+
25
+ * **`read(key)`** read the value for the given key out of the cache or `nil` if the key is not present
26
+ * **`write(key, value)`** write a value to the cache under the key
27
+ * **`exist?(key)`** `true` if the key exists in the cache, `false` otherwise
28
+ * **`clear`** reset the entire cache
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # Copyright (c) 2017-2018 Splitwise Inc.
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #++
25
+
26
+ require 'cacheable/cache_adapter'
27
+ require 'cacheable/cache_adapters'
28
+ require 'cacheable/method_generator'
29
+ require 'cacheable/version'
30
+
31
+ module Cacheable
32
+ extend CacheAdapter
33
+
34
+ def self.included(base)
35
+ base.extend(Cacheable::MethodGenerator)
36
+
37
+ interceptor_name = base.send(:method_interceptor_module_name)
38
+ remove_const(interceptor_name) if const_defined?(interceptor_name)
39
+
40
+ base.prepend const_set(interceptor_name, Module.new)
41
+ end
42
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cacheable
4
+ module CacheAdapter
5
+ CACHE_ADAPTER_METHODS = %i[fetch delete].freeze
6
+ DEFAULT_ADAPTER = :memory
7
+
8
+ def self.extended(base)
9
+ base.instance_variable_set(:@_cache_adapter, nil)
10
+ base.cache_adapter = DEFAULT_ADAPTER
11
+ end
12
+
13
+ def cache_adapter
14
+ @_cache_adapter
15
+ end
16
+
17
+ def cache_adapter=(name_or_adapter)
18
+ @_cache_adapter = interprete_adapter(name_or_adapter)
19
+ end
20
+
21
+ private
22
+
23
+ def interprete_adapter(name_or_adapter)
24
+ return name_or_adapter if cache_adapter?(name_or_adapter)
25
+
26
+ unless [Symbol, String].include?(name_or_adapter.class)
27
+ raise ArgumentError, 'Must pass the name of a known adapter or an instance'
28
+ end
29
+
30
+ Cacheable::CacheAdapters.lookup(name_or_adapter).new
31
+ end
32
+
33
+ def cache_adapter?(adapter_instance)
34
+ CACHE_ADAPTER_METHODS.all? { |method| adapter_instance.respond_to?(method) }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cacheable/cache_adapters/memory_adapter'
4
+
5
+ module Cacheable
6
+ module CacheAdapters
7
+ ADAPTER = 'Adapter'
8
+
9
+ class << self
10
+ def lookup(adapter_name)
11
+ const_get(class_name_for(adapter_name.to_s) + ADAPTER)
12
+ end
13
+
14
+ private
15
+
16
+ def class_name_for(string)
17
+ string.split('_').map { |name_part| "#{name_part[0].upcase}#{name_part[1..-1].downcase}" }.join
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,42 @@
1
+ module Cacheable
2
+ module CacheAdapters
3
+ class MemoryAdapter
4
+ def initialize
5
+ clear
6
+ end
7
+
8
+ def read(key)
9
+ cache[key]
10
+ end
11
+
12
+ def write(key, value)
13
+ cache[key] = value
14
+ end
15
+
16
+ def exist?(key)
17
+ cache.key?(key)
18
+ end
19
+
20
+ def fetch(key, _options = {})
21
+ return read(key) if exist?(key)
22
+
23
+ write(key, Proc.new.call)
24
+ end
25
+
26
+ def delete(key)
27
+ return false unless exist?(key)
28
+
29
+ cache.delete key
30
+ true
31
+ end
32
+
33
+ def clear
34
+ @cache = {}
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :cache
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+ require 'english'
3
+
4
+ module Cacheable
5
+ module MethodGenerator
6
+ def cacheable(*original_method_names, **opts)
7
+ original_method_names.each do |original_method_name|
8
+ create_cacheable_methods(original_method_name, opts)
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def method_interceptor_module_name
15
+ class_name = name || to_s.gsub(/[^a-zA-Z_0-9]/, '')
16
+ "#{class_name}Cacher"
17
+ end
18
+
19
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
20
+ def create_cacheable_methods(original_method_name, opts = {})
21
+ method_names = create_method_names(original_method_name)
22
+ key_format_proc = opts[:key_format] || default_key_format
23
+
24
+ const_get(method_interceptor_module_name).class_eval do
25
+ define_method(method_names[:key_format_method_name]) do |*args|
26
+ key_format_proc.call(self, original_method_name, args)
27
+ end
28
+
29
+ define_method(method_names[:clear_cache_method_name]) do |*args|
30
+ Cacheable.cache_adapter.delete(__send__(method_names[:key_format_method_name], *args))
31
+ end
32
+
33
+ define_method(method_names[:without_cache_method_name]) do |*args|
34
+ original_method = method(original_method_name).super_method
35
+ original_method.call(*args)
36
+ end
37
+
38
+ define_method(method_names[:with_cache_method_name]) do |*args|
39
+ Cacheable.cache_adapter.fetch(__send__(method_names[:key_format_method_name], *args), opts[:cache_options]) do
40
+ __send__(method_names[:without_cache_method_name], *args)
41
+ end
42
+ end
43
+
44
+ define_method(original_method_name) do |*args|
45
+ unless_proc = opts[:unless].is_a?(Symbol) ? opts[:unless].to_proc : opts[:unless]
46
+
47
+ if unless_proc&.call(self, original_method_name, args)
48
+ __send__(method_names[:without_cache_method_name], *args)
49
+ else
50
+ __send__(method_names[:with_cache_method_name], *args)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
56
+
57
+ def default_key_format
58
+ proc do |target, method_name, _method_args|
59
+ # By default, we omit the _method_args from the cache key because there is no acceptable default behavior
60
+ class_name = (target.is_a?(Module) ? target.name : target.class.name)
61
+ cache_key = target.respond_to?(:cache_key) ? target.cache_key : class_name
62
+ [cache_key, method_name].compact
63
+ end
64
+ end
65
+
66
+ def create_method_names(original_method_name)
67
+ method_name_without_punctuation = original_method_name.to_s.sub(/([?!=])$/, '')
68
+ punctuation = $LAST_PAREN_MATCH
69
+
70
+ {
71
+ with_cache_method_name: "#{method_name_without_punctuation}_with_cache#{punctuation}",
72
+ without_cache_method_name: "#{method_name_without_punctuation}_without_cache#{punctuation}",
73
+ key_format_method_name: "#{method_name_without_punctuation}_key_format#{punctuation}",
74
+ clear_cache_method_name: "clear_#{method_name_without_punctuation}_cache#{punctuation}"
75
+ }
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cacheable
4
+ module VERSION
5
+ MAJOR = 1
6
+ MINOR = 0
7
+ TINY = 2
8
+ PRE = nil
9
+
10
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.').freeze
11
+
12
+ def self.to_s
13
+ STRING
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cacheable
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Jess Hottenstein
8
+ - Ryan Laughlin
9
+ - Aaron Rosenberg
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2018-07-31 00:00:00.000000000 Z
14
+ dependencies: []
15
+ description: Add caching simply without modifying your existing code. Inlcudes configurable
16
+ options for simple cache invalidation. See README on github for more information.
17
+ email: support@splitwise.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - README.md
23
+ - cache-adapters.md
24
+ - lib/cacheable.rb
25
+ - lib/cacheable/cache_adapter.rb
26
+ - lib/cacheable/cache_adapters.rb
27
+ - lib/cacheable/cache_adapters/memory_adapter.rb
28
+ - lib/cacheable/method_generator.rb
29
+ - lib/cacheable/version.rb
30
+ homepage: https://github.com/splitwise/cacheable
31
+ licenses:
32
+ - MIT
33
+ metadata: {}
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 2.0.0
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubyforge_project:
50
+ rubygems_version: 2.6.13
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: Add caching to any Ruby method in a aspect orientated programming approach.
54
+ test_files: []
55
+ has_rdoc: