active_remote-cached 1.1.1 → 1.2.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 +4 -4
- data/.github/workflows/ci.yml +85 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +20 -17
- data/Appraisals +27 -0
- data/README.md +121 -0
- data/Rakefile +3 -8
- data/active_remote-cached.gemspec +31 -6
- data/lib/active_remote/cached/argument_keys.rb +61 -27
- data/lib/active_remote/cached/cache.rb +32 -6
- data/lib/active_remote/cached/version.rb +1 -1
- data/lib/active_remote/cached.rb +119 -68
- data/spec/active_remote/cached/argument_keys_spec.rb +124 -0
- data/spec/active_remote/cached/cache_spec.rb +171 -5
- data/spec/active_remote/cached_delete_methods_spec.rb +51 -4
- data/spec/active_remote/cached_exist_methods_spec.rb +75 -4
- data/spec/active_remote/cached_find_methods_spec.rb +6 -8
- data/spec/active_remote/cached_search_methods_spec.rb +57 -8
- data/spec/active_remote/cached_spec.rb +331 -0
- data/spec/spec_helper.rb +1 -0
- metadata +48 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db04ead0732ea2b7c372a7c7f674a8d3450c483a05688a56ee3684f2dc7c2688
|
|
4
|
+
data.tar.gz: 6d0823e6ff844c6effb2da7dddc15353da1240a694cc88324a1ed9c1d273189c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 760fe52b8cc4b6b1e3f5d7bb61af29567f0a8deb99ea0d4b29941bed11f293c1ef43667dcbefb9bf7a18423680871b7dbcd19ef314a9a7e58976385ae0305d59
|
|
7
|
+
data.tar.gz: 544d6d1e7d17afb3bb125af5d2e66efd41eae5676cd6984491fa9c611d0a0c258e8e8a69020adb13b9ad240dd4f4ab46e548defcd6b06194eee4789ee4402868
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- master
|
|
7
|
+
pull_request:
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: true
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
test:
|
|
16
|
+
name: ${{ matrix.ruby }} / ${{ matrix.appraisal }}
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
env:
|
|
19
|
+
BUNDLE_GEMFILE: ${{ github.workspace }}/Gemfile
|
|
20
|
+
strategy:
|
|
21
|
+
fail-fast: false
|
|
22
|
+
matrix:
|
|
23
|
+
ruby:
|
|
24
|
+
- '3.1'
|
|
25
|
+
- '3.4'
|
|
26
|
+
- jruby-9.4
|
|
27
|
+
- jruby-10.0
|
|
28
|
+
appraisal:
|
|
29
|
+
- active_remote-6.1
|
|
30
|
+
- active_remote-7.0
|
|
31
|
+
- active_remote-7.1
|
|
32
|
+
- active_remote-7.2
|
|
33
|
+
- active_remote-8.0
|
|
34
|
+
exclude:
|
|
35
|
+
# active_remote 8.0 requires ruby 3.2 or later.
|
|
36
|
+
# jruby-9.4 is ruby 3.1 compatible.
|
|
37
|
+
- ruby: '3.1'
|
|
38
|
+
appraisal: active_remote-8.0
|
|
39
|
+
- ruby: jruby-9.4
|
|
40
|
+
appraisal: active_remote-8.0
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
|
|
44
|
+
- name: Set up Ruby
|
|
45
|
+
uses: ruby/setup-ruby@v1
|
|
46
|
+
with:
|
|
47
|
+
ruby-version: ${{ matrix.ruby }}
|
|
48
|
+
bundler-cache: true
|
|
49
|
+
|
|
50
|
+
- name: Generate the appraisal gemfiles
|
|
51
|
+
run: bundle exec appraisal generate
|
|
52
|
+
|
|
53
|
+
# bundler-cache above only caches the root Gemfile. The appraisal
|
|
54
|
+
# gemfiles do not exist until the step above runs, so they need their own
|
|
55
|
+
# cache. Without this every job installs active_remote from cold.
|
|
56
|
+
- name: Cache the appraisal dependencies
|
|
57
|
+
uses: actions/cache@v4
|
|
58
|
+
with:
|
|
59
|
+
path: gemfiles/vendor/bundle
|
|
60
|
+
key: ${{ runner.os }}-${{ matrix.ruby }}-${{ matrix.appraisal }}-${{ hashFiles('Appraisals', '*.gemspec') }}
|
|
61
|
+
restore-keys: |
|
|
62
|
+
${{ runner.os }}-${{ matrix.ruby }}-${{ matrix.appraisal }}-
|
|
63
|
+
|
|
64
|
+
- name: Install the appraisal dependencies
|
|
65
|
+
run: |
|
|
66
|
+
bundle exec appraisal ${{ matrix.appraisal }} bundle config set --local path "$GITHUB_WORKSPACE/gemfiles/vendor/bundle"
|
|
67
|
+
bundle exec appraisal ${{ matrix.appraisal }} bundle install --jobs 4 --retry 3
|
|
68
|
+
|
|
69
|
+
- name: Run the specs
|
|
70
|
+
run: bundle exec appraisal ${{ matrix.appraisal }} rspec
|
|
71
|
+
|
|
72
|
+
rubocop:
|
|
73
|
+
name: RuboCop
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/checkout@v4
|
|
77
|
+
|
|
78
|
+
- name: Set up Ruby
|
|
79
|
+
uses: ruby/setup-ruby@v1
|
|
80
|
+
with:
|
|
81
|
+
ruby-version: '3.4'
|
|
82
|
+
bundler-cache: true
|
|
83
|
+
|
|
84
|
+
- name: Run RuboCop
|
|
85
|
+
run: bundle exec rubocop --parallel
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
AllCops:
|
|
2
2
|
TargetRubyVersion: 3.1
|
|
3
|
+
# Do not adopt new cops automatically. Without this, every run prints the
|
|
4
|
+
# full pending cop list as a warning.
|
|
5
|
+
NewCops: disable
|
|
6
|
+
# Keep the default Exclude list. Without this, the Exclude below replaces the
|
|
7
|
+
# defaults, and RuboCop walks into vendor/bundle and reads the .rubocop.yml of
|
|
8
|
+
# every installed gem. Some of those name cops that no longer exist.
|
|
9
|
+
inherit_mode:
|
|
10
|
+
merge:
|
|
11
|
+
- Exclude
|
|
3
12
|
Exclude:
|
|
4
13
|
- 'gemfiles/**/*'
|
|
5
14
|
|
|
6
|
-
Layout/AccessModifierIndentation:
|
|
7
|
-
Enabled: false
|
|
8
|
-
Layout/CommentIndentation:
|
|
9
|
-
Enabled: false
|
|
10
|
-
Layout/IndentationConsistency:
|
|
11
|
-
Enabled: false
|
|
12
|
-
|
|
13
|
-
Style/AccessModifierDeclarations:
|
|
14
|
-
Enabled: false
|
|
15
|
-
|
|
16
15
|
Naming/MethodParameterName:
|
|
17
16
|
Enabled: false
|
|
18
17
|
|
|
19
18
|
Metrics/BlockLength:
|
|
20
19
|
Exclude:
|
|
21
20
|
- 'spec/**/*'
|
|
21
|
+
- '*.gemspec'
|
|
22
|
+
|
|
23
|
+
# ActiveSupport cache #fetch only calls the block on a miss, so the block form
|
|
24
|
+
# is not the same as the Hash#fetch default value form.
|
|
25
|
+
Style/RedundantFetchBlock:
|
|
26
|
+
Exclude:
|
|
27
|
+
- 'spec/active_remote/cached/cache_spec.rb'
|
|
22
28
|
|
|
23
29
|
Naming/FileName:
|
|
24
30
|
Exclude:
|
|
@@ -32,11 +38,11 @@ Metrics/ModuleLength:
|
|
|
32
38
|
Style/Documentation:
|
|
33
39
|
Enabled: false
|
|
34
40
|
|
|
41
|
+
# The generated finder methods in this file are long heredocs.
|
|
35
42
|
Metrics/MethodLength:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Enabled: false
|
|
43
|
+
Exclude:
|
|
44
|
+
- 'lib/active_remote/cached.rb'
|
|
45
|
+
- 'spec/**/*'
|
|
40
46
|
|
|
41
47
|
Style/HashSyntax:
|
|
42
48
|
Description: >-
|
|
@@ -50,6 +56,3 @@ Style/HashSyntax:
|
|
|
50
56
|
# Use lambdas instead of stabbys
|
|
51
57
|
Style/Lambda:
|
|
52
58
|
EnforcedStyle: lambda
|
|
53
|
-
|
|
54
|
-
Style/MissingRespondToMissing:
|
|
55
|
-
Enabled: false
|
data/Appraisals
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
appraise 'active_remote-6.1' do
|
|
4
|
+
gem 'active_remote', '~> 6.1.0'
|
|
5
|
+
# activesupport 6.1 is not compatible with concurrent-ruby 1.3.5 and later
|
|
6
|
+
gem 'concurrent-ruby', '< 1.3.5'
|
|
7
|
+
# ruby 3.4 removed these from the default gems, activesupport 6.1 still needs them
|
|
8
|
+
gem 'base64'
|
|
9
|
+
gem 'bigdecimal'
|
|
10
|
+
gem 'mutex_m'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
appraise 'active_remote-7.0' do
|
|
14
|
+
gem 'active_remote', '~> 7.0.0'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
appraise 'active_remote-7.1' do
|
|
18
|
+
gem 'active_remote', '~> 7.1.0'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
appraise 'active_remote-7.2' do
|
|
22
|
+
gem 'active_remote', '~> 7.2.0'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
appraise 'active_remote-8.0' do
|
|
26
|
+
gem 'active_remote', '~> 8.0.0'
|
|
27
|
+
end
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# ActiveRemote::Cached
|
|
2
2
|
|
|
3
|
+
[](https://github.com/skunkworker/active_remote-cached/actions/workflows/ci.yml)
|
|
4
|
+
|
|
3
5
|
Provides cached finders for ActiveRemote models that allow a caching provider to cache the result of a query.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
@@ -88,6 +90,125 @@ Each finder as takes an optional options hash that will override the options pas
|
|
|
88
90
|
customer = ::Customer.cached_find_by_id(1, :expires_in => 15.minutes)
|
|
89
91
|
```
|
|
90
92
|
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
Install the dependencies:
|
|
96
|
+
|
|
97
|
+
```shell
|
|
98
|
+
bundle install
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Run the specs against the default gemfile:
|
|
102
|
+
|
|
103
|
+
```shell
|
|
104
|
+
bundle exec rspec
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Run RuboCop:
|
|
108
|
+
|
|
109
|
+
```shell
|
|
110
|
+
bundle exec rubocop
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Test matrix
|
|
114
|
+
|
|
115
|
+
This gem uses [appraisal](https://github.com/thoughtbot/appraisal) to test against
|
|
116
|
+
several `active_remote` versions. The `Appraisals` file defines each version.
|
|
117
|
+
|
|
118
|
+
Generate the gemfiles. They are not committed:
|
|
119
|
+
|
|
120
|
+
```shell
|
|
121
|
+
bundle exec appraisal generate
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Install every appraisal:
|
|
125
|
+
|
|
126
|
+
```shell
|
|
127
|
+
bundle exec appraisal install
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Run the specs against every appraisal:
|
|
131
|
+
|
|
132
|
+
```shell
|
|
133
|
+
bundle exec appraisal rspec
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Run the specs against one appraisal:
|
|
137
|
+
|
|
138
|
+
```shell
|
|
139
|
+
bundle exec appraisal active_remote-8.0 rspec
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Remove the generated gemfiles:
|
|
143
|
+
|
|
144
|
+
```shell
|
|
145
|
+
bundle exec appraisal clean
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
CI runs this matrix on Ruby 3.1, Ruby 3.4, JRuby 9.4, and JRuby 10.0.
|
|
149
|
+
`active_remote` 8.0 requires Ruby 3.2 or later. CI does not run that
|
|
150
|
+
version on Ruby 3.1 or JRuby 9.4.
|
|
151
|
+
|
|
152
|
+
## Upgrading to 1.2.0
|
|
153
|
+
|
|
154
|
+
### Every cache key changes
|
|
155
|
+
|
|
156
|
+
Before 1.2.0 the cache key held only the argument values, joined with no
|
|
157
|
+
separator. Three different finders shared one cache entry:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
Customer.cached_find_by_name_and_email("x", "y") # key: "xy"
|
|
161
|
+
Customer.cached_find_by_city_and_state("x", "y") # key: "xy" same entry
|
|
162
|
+
Customer.cached_find_by_id("xy") # key: "xy" same entry
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The key now names each field, so each finder gets its own entry:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
Customer.cached_find_by_name_and_email("x", "y") # key: "email.y/name.x"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Every existing cache entry becomes a miss after the upgrade. Expect one cold
|
|
172
|
+
period. The gem already causes this on an ActiveSupport upgrade, through
|
|
173
|
+
`RUBY_AND_ACTIVE_SUPPORT_VERSION`.
|
|
174
|
+
|
|
175
|
+
### A bad call now raises
|
|
176
|
+
|
|
177
|
+
A dynamic finder called with too few arguments used to pass `nil` for the
|
|
178
|
+
missing field and cache the result. It now raises `ArgumentError`:
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
Customer.cached_find_by_email_and_name("only_one") # => ArgumentError
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### The cache provider validator raises a new class
|
|
185
|
+
|
|
186
|
+
`ActiveRemote::Cached::Cache::InvalidCacheProvider` replaces the bare
|
|
187
|
+
`RuntimeError` that `ActiveRemote::Cached.cache` raised for a provider that is
|
|
188
|
+
missing a method.
|
|
189
|
+
|
|
190
|
+
## Known behavior
|
|
191
|
+
|
|
192
|
+
Two behaviors are recorded in the specs. Neither is fixed. Read
|
|
193
|
+
`spec/active_remote/cached_spec.rb` for the specs that describe them.
|
|
194
|
+
|
|
195
|
+
### Finder name matching is not anchored
|
|
196
|
+
|
|
197
|
+
`_method_missing_name` matches a finder name inside a longer method name. A
|
|
198
|
+
method named `not_cached_find_by_guid` resolves to `cached_find_by_guid`.
|
|
199
|
+
|
|
200
|
+
### A subclass has its own empty cached_methods list
|
|
201
|
+
|
|
202
|
+
A subclass inherits the finder methods its parent defined, and the options
|
|
203
|
+
those finders were declared with. It does not inherit the `cached_methods`
|
|
204
|
+
list. The parent accepts the finder arguments in any order. The subclass
|
|
205
|
+
accepts them only in the order the method was defined.
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
Parent.cached_find_by_beta_and_alpha('B', 'A') # works
|
|
209
|
+
Child.cached_find_by_beta_and_alpha('B', 'A') # raises NoMethodError
|
|
210
|
+
```
|
|
211
|
+
|
|
91
212
|
## Contributing
|
|
92
213
|
|
|
93
214
|
1. Fork it
|
data/Rakefile
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'bundler/gem_tasks'
|
|
4
|
-
require '
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
|
+
require 'appraisal'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
t.libs.push 'lib'
|
|
8
|
-
t.libs.push 'spec'
|
|
9
|
-
t.pattern = 'spec/**/*_spec.rb'
|
|
10
|
-
t.verbose = true
|
|
11
|
-
end
|
|
7
|
+
::RSpec::Core::RakeTask.new(:spec)
|
|
12
8
|
|
|
13
|
-
task :spec => :test
|
|
14
9
|
task :default => :spec
|
|
@@ -6,6 +6,21 @@ lib = File.expand_path('lib', __dir__)
|
|
|
6
6
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
7
7
|
require 'active_remote/cached/version'
|
|
8
8
|
|
|
9
|
+
HOMEPAGE = 'https://github.com/mxenabled/active_remote-cached'
|
|
10
|
+
|
|
11
|
+
# git ls-files returns nothing outside a checkout, so a build from a released
|
|
12
|
+
# tarball needs the glob.
|
|
13
|
+
def gem_files
|
|
14
|
+
files = if File.directory?(File.join(__dir__, '.git'))
|
|
15
|
+
`git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
|
16
|
+
else
|
|
17
|
+
Dir.glob('{lib,spec}/**/*', File::FNM_DOTMATCH) +
|
|
18
|
+
%w[LICENSE.txt README.md Rakefile Appraisals active_remote-cached.gemspec]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
files.reject { |file| File.directory?(file) }
|
|
22
|
+
end
|
|
23
|
+
|
|
9
24
|
Gem::Specification.new do |gem|
|
|
10
25
|
gem.name = 'active_remote-cached'
|
|
11
26
|
gem.version = ActiveRemote::Cached::VERSION
|
|
@@ -13,21 +28,31 @@ Gem::Specification.new do |gem|
|
|
|
13
28
|
gem.email = ['brandonsdewitt@gmail.com', 'devexperience@mx.com']
|
|
14
29
|
gem.description = ' Provides "cached" finders and a DSL to enumerate which finders should have cached versions '
|
|
15
30
|
gem.summary = ' Provides a configuration for caching mechanisms and finders on ActiveRemote models'
|
|
16
|
-
gem.homepage =
|
|
31
|
+
gem.homepage = HOMEPAGE
|
|
32
|
+
gem.license = 'MIT'
|
|
33
|
+
|
|
34
|
+
gem.metadata = {
|
|
35
|
+
'homepage_uri' => HOMEPAGE,
|
|
36
|
+
'source_code_uri' => HOMEPAGE,
|
|
37
|
+
'rubygems_mfa_required' => 'true'
|
|
38
|
+
}
|
|
17
39
|
|
|
18
|
-
gem.required_ruby_version = '>=
|
|
19
|
-
gem.files =
|
|
40
|
+
gem.required_ruby_version = '>= 3.1'
|
|
41
|
+
gem.files = gem_files
|
|
20
42
|
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
|
21
|
-
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
22
43
|
gem.require_paths = ['lib']
|
|
23
44
|
|
|
24
45
|
gem.add_dependency 'active_remote', '>= 6.1'
|
|
25
|
-
|
|
46
|
+
# NullStore and ActiveSupport::VERSION::STRING. Matches the active_remote floor.
|
|
47
|
+
gem.add_dependency 'activesupport', '>= 6.1'
|
|
26
48
|
|
|
49
|
+
gem.add_development_dependency 'appraisal'
|
|
27
50
|
gem.add_development_dependency 'bundler'
|
|
28
51
|
gem.add_development_dependency 'mocha'
|
|
52
|
+
# ostruct leaves the default gems in ruby 4.0
|
|
53
|
+
gem.add_development_dependency 'ostruct'
|
|
29
54
|
gem.add_development_dependency 'pry'
|
|
30
55
|
gem.add_development_dependency 'rake'
|
|
31
56
|
gem.add_development_dependency 'rspec', '>= 3.0'
|
|
32
|
-
gem.add_development_dependency 'rubocop'
|
|
57
|
+
gem.add_development_dependency 'rubocop', '~> 1.80'
|
|
33
58
|
end
|
|
@@ -1,45 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ActiveRemote
|
|
2
4
|
module Cached
|
|
3
5
|
class ArgumentKeys
|
|
4
6
|
attr_reader :arguments, :argument_string, :options
|
|
5
7
|
|
|
6
8
|
REMOVE_CHARACTERS = /[[:space:]+=><{}\[\];:\-,]/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
# Covers the same characters as REMOVE_CHARACTERS. A tab or a newline
|
|
10
|
+
# left in a key breaks the Memcached protocol.
|
|
11
|
+
REPLACE_MAP = {
|
|
12
|
+
' ' => 'SP',
|
|
13
|
+
"\t" => 'TB',
|
|
14
|
+
"\n" => 'NL',
|
|
15
|
+
"\r" => 'CR',
|
|
16
|
+
"\f" => 'FF',
|
|
17
|
+
"\v" => 'VT',
|
|
18
|
+
'+' => 'PL',
|
|
19
|
+
'=' => 'EQ',
|
|
20
|
+
'>' => 'GT',
|
|
21
|
+
'<' => 'LT',
|
|
22
|
+
'{' => 'LB',
|
|
23
|
+
'}' => 'RB',
|
|
24
|
+
'[' => 'LB2',
|
|
25
|
+
']' => 'RB2',
|
|
26
|
+
';' => 'SC',
|
|
27
|
+
':' => 'CO',
|
|
28
|
+
'-' => 'DA',
|
|
29
|
+
',' => 'COM'
|
|
30
|
+
}.freeze
|
|
31
|
+
REPLACE_CHARACTERS = ::Regexp.union(REPLACE_MAP.keys)
|
|
32
|
+
|
|
33
|
+
# The separators below are absent from both REMOVE_CHARACTERS and
|
|
34
|
+
# REPLACE_MAP, so they survive either option. escape_value/1 escapes them
|
|
35
|
+
# inside a value, which makes the key one-to-one with the arguments.
|
|
36
|
+
FIELD_SEPARATOR = '.'
|
|
37
|
+
PAIR_SEPARATOR = '/'
|
|
38
|
+
ESCAPE_MAP = { '%' => '%25', FIELD_SEPARATOR => '%2E', PAIR_SEPARATOR => '%2F' }.freeze
|
|
39
|
+
ESCAPE_CHARACTERS = %r{[%./]}
|
|
40
|
+
|
|
41
|
+
# Build a key that names each field, so that two finders with the same
|
|
42
|
+
# values do not share one cache entry.
|
|
43
|
+
#
|
|
44
|
+
# for_fields([:alpha, :beta], ['x', 'y'], {}).cache_key
|
|
45
|
+
# # => "alpha.x/beta.y"
|
|
46
|
+
#
|
|
47
|
+
def self.for_fields(field_names, values, options)
|
|
48
|
+
pairs = field_names.each_with_index.map do |field_name, index|
|
|
49
|
+
"#{field_name}#{FIELD_SEPARATOR}#{normalize_value(values[index])}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
new(pairs.join(PAIR_SEPARATOR), options)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.normalize_value(value)
|
|
56
|
+
[value].flatten.compact.map { |element| escape_value(element) }.join(FIELD_SEPARATOR)
|
|
57
|
+
end
|
|
58
|
+
private_class_method :normalize_value
|
|
59
|
+
|
|
60
|
+
def self.escape_value(value)
|
|
61
|
+
value.to_s.gsub(ESCAPE_CHARACTERS, ESCAPE_MAP)
|
|
62
|
+
end
|
|
63
|
+
private_class_method :escape_value
|
|
22
64
|
|
|
23
65
|
def initialize(*arguments, options)
|
|
24
66
|
@options = options
|
|
25
67
|
@arguments = arguments.flatten.compact
|
|
26
|
-
@argument_string =
|
|
27
|
-
|
|
28
|
-
@arguments.each do |argument|
|
|
29
|
-
@argument_string << argument.to_s
|
|
30
|
-
end
|
|
68
|
+
@argument_string = @arguments.join
|
|
31
69
|
end
|
|
32
70
|
|
|
33
71
|
def cache_key
|
|
34
72
|
return @argument_string.gsub(REMOVE_CHARACTERS, '') if remove_characters?
|
|
73
|
+
return @argument_string unless replace_characters?
|
|
35
74
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@argument_string.gsub!(character, replacement)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
@argument_string
|
|
75
|
+
# One pass, rather than one gsub for each entry in the map.
|
|
76
|
+
@argument_string.gsub(REPLACE_CHARACTERS, REPLACE_MAP)
|
|
43
77
|
end
|
|
44
78
|
|
|
45
79
|
def to_s
|
|
@@ -5,6 +5,10 @@ require 'delegate'
|
|
|
5
5
|
module ActiveRemote
|
|
6
6
|
module Cached
|
|
7
7
|
class Cache < ::SimpleDelegator
|
|
8
|
+
# Raised when the given cache provider is missing a method the library
|
|
9
|
+
# calls on it.
|
|
10
|
+
class InvalidCacheProvider < ::StandardError; end
|
|
11
|
+
|
|
8
12
|
attr_reader :cache_provider
|
|
9
13
|
|
|
10
14
|
def initialize(new_cache_provider)
|
|
@@ -29,14 +33,19 @@ module ActiveRemote
|
|
|
29
33
|
@nested_cache_provider = ::ActiveSupport::Cache::MemoryStore.new
|
|
30
34
|
end
|
|
31
35
|
|
|
36
|
+
def nested_caching?
|
|
37
|
+
!nested_cache_provider.is_a?(::ActiveSupport::Cache::NullStore)
|
|
38
|
+
end
|
|
39
|
+
|
|
32
40
|
def exist?(*args)
|
|
33
41
|
nested_cache_provider.exist?(*args) || super
|
|
34
42
|
end
|
|
35
43
|
|
|
36
44
|
def fetch(name, options = {})
|
|
37
|
-
|
|
45
|
+
provider_options = provider_fetch_options(options)
|
|
46
|
+
fetch_value = nested_cache_provider.fetch(name, provider_options) { super(name, provider_options) }
|
|
38
47
|
|
|
39
|
-
delete(name)
|
|
48
|
+
delete(name) if delete_after_fetch?(fetch_value, options, provider_options)
|
|
40
49
|
|
|
41
50
|
fetch_value
|
|
42
51
|
end
|
|
@@ -54,6 +63,24 @@ module ActiveRemote
|
|
|
54
63
|
|
|
55
64
|
attr_reader :nested_cache_provider
|
|
56
65
|
|
|
66
|
+
# :skip_nil tells the provider not to write a nil at all, which saves a
|
|
67
|
+
# write and the delete that follows it. Only an ActiveSupport store is
|
|
68
|
+
# known to honor the option.
|
|
69
|
+
def provider_fetch_options(options)
|
|
70
|
+
return options if options.fetch(:allow_nil, false)
|
|
71
|
+
return options unless cache_provider.is_a?(::ActiveSupport::Cache::Store)
|
|
72
|
+
|
|
73
|
+
options.merge(:skip_nil => true)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def delete_after_fetch?(value, options, provider_options)
|
|
77
|
+
return false if valid_fetched_value?(value, options)
|
|
78
|
+
# The provider already skipped the write.
|
|
79
|
+
return false if value.nil? && provider_options[:skip_nil]
|
|
80
|
+
|
|
81
|
+
true
|
|
82
|
+
end
|
|
83
|
+
|
|
57
84
|
def valid_fetched_value?(value, options = {})
|
|
58
85
|
return false if value.nil? && !options.fetch(:allow_nil, false)
|
|
59
86
|
return false if !options.fetch(:allow_empty, false) && value.respond_to?(:empty?) && value.empty?
|
|
@@ -64,10 +91,9 @@ module ActiveRemote
|
|
|
64
91
|
def validate_provider_method_present(method_name)
|
|
65
92
|
return if cache_provider.respond_to?(method_name)
|
|
66
93
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
CACHE_METHOD
|
|
94
|
+
raise InvalidCacheProvider,
|
|
95
|
+
"ActiveRemote::Cached::Cache must respond_to? #{method_name} " \
|
|
96
|
+
'in order to be used as a caching interface for ActiveRemote'
|
|
71
97
|
end
|
|
72
98
|
end
|
|
73
99
|
end
|