thread_cache 1.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5c5461aafc736ae8c0b33ac3da0a1c1db3e49a28b4e7aac55dd10e2dca73b933
4
+ data.tar.gz: d9802d83b6663e6733f9e795f64cd7f1bd9b40e82397cc3168accd247eec5c18
5
+ SHA512:
6
+ metadata.gz: ec97f1f5c73528c13fe7d16b674a0e913622e529b3d06eb372a00210e36b4a54d46be42f405aba11ca018de24e0bdf72ed6b106130d590da4bfd4121abb3846b
7
+ data.tar.gz: '09f4e1ca3e71c59fd8db2ba9f276a2bfa3c4d2924e0205c6791539795b130f121c6a8edfa3201a5208d172f95e5b13a1671340a9ca6111f07231c0cf973f5683'
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ coverage/
2
+ *.gem
data/.rubocop.yml ADDED
@@ -0,0 +1,48 @@
1
+ require: rubocop-performance
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 2.5
5
+ SuggestExtensions: false
6
+ NewCops: enable
7
+
8
+ Metrics/BlockLength:
9
+ Max: 30
10
+ Exclude:
11
+ - 'spec/**/*.rb'
12
+ Metrics/MethodLength:
13
+ Max: 30
14
+ Metrics/ClassLength:
15
+ Max: 200
16
+ Metrics/AbcSize:
17
+ Max: 20
18
+
19
+ Layout/LineLength:
20
+ Max: 120
21
+ Layout/FirstHashElementIndentation:
22
+ Enabled: false
23
+ Layout/HashAlignment:
24
+ Enabled: false
25
+
26
+ Naming/AccessorMethodName:
27
+ Enabled: false
28
+ Naming/MethodParameterName:
29
+ Enabled: false
30
+
31
+ Style/Documentation:
32
+ Enabled: false
33
+ Style/WordArray:
34
+ Enabled: false
35
+ Style/SymbolArray:
36
+ Enabled: false
37
+ Style/NumericLiterals:
38
+ Enabled: false
39
+ Style/StringLiterals:
40
+ Enabled: false
41
+ Style/TrailingCommaInArrayLiteral:
42
+ Enabled: false
43
+ Style/TrailingCommaInHashLiteral:
44
+ Enabled: false
45
+ Style/NegatedIf:
46
+ Enabled: false
47
+ Style/IfUnlessModifier:
48
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Elias Rodrigues
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # ThreadCache
2
+
3
+ A simple thread-local cache store
4
+
5
+ ## Install
6
+
7
+ Install it from rubygems.org in your terminal:
8
+
9
+ ```sh
10
+ gem install thread_cache
11
+ ```
12
+
13
+ Or via `Gemfile` in your project:
14
+
15
+ ```sh
16
+ source 'https://rubygems.org'
17
+
18
+ gem 'thread_cache', '~> 1.0'
19
+ ```
20
+
21
+ Or build and install the gem locally:
22
+
23
+ ```sh
24
+ gem build thread_cache.gemspec
25
+ gem install thread_cache-1.0.0.gem
26
+ ```
27
+
28
+ Require it in your Ruby code and the `ThreadCache` class will be available:
29
+
30
+ ```rb
31
+ require 'thread_cache'
32
+ ```
33
+
34
+ ## Tests
35
+
36
+ Run tests with:
37
+
38
+ ```sh
39
+ bundle exec rspec
40
+ ```
41
+
42
+ ## Linter
43
+
44
+ Check your code with:
45
+
46
+ ```sh
47
+ bundle exec rubocop
48
+ ```
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ThreadCache
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './thread_cache/version'
4
+
5
+ # A simple thread-local cache store
6
+ #
7
+ # options:
8
+ # namespace: Thread attribute name to use as data store (String or Symbol)
9
+ # expires_in: Default number of seconds to expire values (number)
10
+ # skip_nil: Whether or not to cache nil values by default (boolean)
11
+ class ThreadCache
12
+ def initialize(options = {})
13
+ @namespace = options.fetch(:namespace, 'thread_cache')
14
+ @expires_in = options.fetch(:expires_in, 60)
15
+ @skip_nil = options.fetch(:skip_nil, false)
16
+
17
+ data_store
18
+ end
19
+
20
+ def clear
21
+ data_store.clear
22
+ end
23
+
24
+ def exist?(key)
25
+ data_store.key?(key)
26
+ end
27
+
28
+ # Writes a key-value pair to the data store.
29
+ #
30
+ # NOTE: value is written as it is given, it does not duplicate nor serialize it.
31
+ #
32
+ # key: (String or Symbol)
33
+ # value: (any type)
34
+ # options:
35
+ # version: sets the value's version (any type)
36
+ # expires_in: overrides the default expires_in
37
+ # skip_nil: overrides the default skip_nil
38
+ def write(key, value, options = {})
39
+ perform_write(key, value, parse_options(options))
40
+ end
41
+
42
+ # Reads a value from the data store using key.
43
+ #
44
+ # options:
45
+ # version: defines the version to read the value
46
+ def read(key, options = {})
47
+ options = parse_options(options)
48
+
49
+ entry = data_store[key]
50
+ value, _error = validate(key, entry, options[:version])
51
+ value
52
+ end
53
+
54
+ # Fetches a value from the data store using key.
55
+ # If a valid value is not found, it writes the value returned from the given block.
56
+ #
57
+ # options:
58
+ # force: writes the value even when a valid value is found (boolean)
59
+ # version: sets the value's version
60
+ # expires_in: overrides the default expires_in
61
+ # skip_nil: overrides the default skip_nil
62
+ def fetch(key, options = {}, &block)
63
+ options = parse_options(options)
64
+
65
+ perform_fetch(key, options, &block)
66
+ end
67
+
68
+ def delete(key)
69
+ entry = data_store.delete(key)
70
+
71
+ !entry.nil?
72
+ end
73
+
74
+ def write_multi(keys_and_values, options = {})
75
+ options = parse_options_multi(keys_and_values.keys, options)
76
+
77
+ keys_and_values.each do |key, value|
78
+ opts = {
79
+ version: options[:version][key],
80
+ expires_in: options[:expires_in][key],
81
+ skip_nil: options[:skip_nil][key],
82
+ }
83
+ perform_write(key, value, opts)
84
+ end
85
+ end
86
+
87
+ def read_multi(keys, options = {})
88
+ options = parse_options_multi(keys, options)
89
+
90
+ entries = data_store.slice(*keys)
91
+ keys.each_with_object({}) do |key, acc|
92
+ value, _error = validate(key, entries[key], options[:version][key])
93
+ acc[key] = value
94
+ end
95
+ end
96
+
97
+ def fetch_multi(keys, options = {}, &block)
98
+ options = parse_options_multi(keys, options)
99
+
100
+ keys.each_with_object({}) do |key, acc|
101
+ opts = {
102
+ force: options[:force][key],
103
+ version: options[:version][key],
104
+ expires_in: options[:expires_in][key],
105
+ skip_nil: options[:skip_nil][key],
106
+ }
107
+ acc[key] = perform_fetch(key, opts, &block)
108
+ end
109
+ end
110
+
111
+ def delete_multi(keys)
112
+ keys.map { |key| delete(key) }
113
+ end
114
+
115
+ def delete_matched(pattern)
116
+ data_store.keys.each_with_object([]) do |key, acc|
117
+ if key.match?(pattern)
118
+ delete(key)
119
+ acc << key
120
+ end
121
+ end
122
+ end
123
+
124
+ # Validates all entries, consequently deleting the ones that have expired.
125
+ #
126
+ # options:
127
+ # version: defines the version to read the values
128
+ def cleanup(options = {})
129
+ options = parse_options(options)
130
+
131
+ data_store.map.each_with_object([]) do |(key, entry), acc|
132
+ _value, error = validate(key, entry, options[:version])
133
+ acc << key if !error.nil?
134
+ end
135
+ end
136
+
137
+ def increment(key, amount = 1, options = {})
138
+ options = parse_options(options)
139
+
140
+ perform_add(key, amount, options)
141
+ end
142
+
143
+ def decrement(key, amount = 1, options = {})
144
+ options = parse_options(options)
145
+
146
+ perform_add(key, -amount, options)
147
+ end
148
+
149
+ alias exists? exist?
150
+
151
+ alias set write
152
+ alias set_multi write_multi
153
+
154
+ alias get read
155
+ alias get_multi read_multi
156
+
157
+ alias remove delete
158
+ alias remove_multi delete_multi
159
+ alias remove_matched delete_matched
160
+
161
+ alias incr increment
162
+ alias decr decrement
163
+
164
+ private
165
+
166
+ def data_store
167
+ Thread.current[@namespace] ||= {}
168
+ end
169
+
170
+ def build_entry(value, version, expires_in)
171
+ {
172
+ value: value,
173
+ version: version,
174
+ expires_in: expires_in&.to_f,
175
+ created_at: current_unix_time,
176
+ }
177
+ end
178
+
179
+ def perform_write(key, value, options)
180
+ return if value.nil? && options[:skip_nil]
181
+
182
+ data_store[key] = build_entry(value, options[:version], options[:expires_in])
183
+ value
184
+ end
185
+
186
+ def perform_fetch(key, options)
187
+ if options[:force]
188
+ perform_write(key, yield(key), options)
189
+ else
190
+ entry = data_store[key]
191
+ value, error = validate(key, entry, options[:version])
192
+
193
+ if error.nil?
194
+ value
195
+ else
196
+ perform_write(key, yield(key), options)
197
+ end
198
+ end
199
+ end
200
+
201
+ def perform_add(key, amount, options = {})
202
+ entry = data_store[key]
203
+ value, _error = validate(key, entry, options[:version])
204
+
205
+ perform_write(key, value.to_i + amount, options)
206
+ end
207
+
208
+ def validate(key, entry, version)
209
+ if entry.nil?
210
+ [nil, 'not found']
211
+ elsif expired?(entry) || mismatched?(entry, version)
212
+ delete(key)
213
+ [nil, 'expired or mismatched']
214
+ else
215
+ [entry[:value], nil]
216
+ end
217
+ end
218
+
219
+ def expired?(entry)
220
+ entry[:expires_in] && entry[:created_at] + entry[:expires_in] <= current_unix_time
221
+ end
222
+
223
+ def mismatched?(entry, version)
224
+ entry[:version] && version && entry[:version] != version
225
+ end
226
+
227
+ def current_unix_time
228
+ Time.now.to_f
229
+ end
230
+
231
+ def options_defaults
232
+ @options_defaults ||= {
233
+ force: nil,
234
+ version: nil,
235
+ expires_in: @expires_in,
236
+ skip_nil: @skip_nil,
237
+ }
238
+ end
239
+
240
+ def parse_options(options)
241
+ options_defaults.each_with_object({}) do |(option_name, default_value), acc|
242
+ acc[option_name] = options.fetch(option_name, default_value)
243
+ end
244
+ end
245
+
246
+ def parse_options_multi(keys, options)
247
+ options_defaults.each_with_object({}) do |(option_name, default_value), acc|
248
+ option_value = options.fetch(option_name, default_value)
249
+
250
+ acc[option_name] = option_value_for_multi(keys, option_value, default_value)
251
+ end
252
+ end
253
+
254
+ def option_value_for_multi(keys, option_value, fallback_value)
255
+ case option_value
256
+ when Hash
257
+ (Hash.new { fallback_value }).merge(option_value)
258
+ when Array
259
+ (Hash.new { fallback_value }).merge(keys.take(option_value.size).zip(option_value).to_h)
260
+ else
261
+ Hash.new { option_value }
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'simplecov'
4
+ SimpleCov.start
5
+
6
+ require 'rspec'
7
+ require 'active_support/testing/time_helpers'
8
+ require 'pry-byebug'
9
+
10
+ RSpec.configure do |config|
11
+ config.include ActiveSupport::Testing::TimeHelpers
12
+ end