active_remote-cached 1.0.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c89a29c20fea4fff1a59ab534285774e7758620a73fc4005866011326b67ddbe
4
- data.tar.gz: 1fbb57ba895a2aedb8067e399ac747cd6428b2e11c18c043e0fc9fbd8e3a63a1
3
+ metadata.gz: db04ead0732ea2b7c372a7c7f674a8d3450c483a05688a56ee3684f2dc7c2688
4
+ data.tar.gz: 6d0823e6ff844c6effb2da7dddc15353da1240a694cc88324a1ed9c1d273189c
5
5
  SHA512:
6
- metadata.gz: e07972d8b297ad00be81d09dc05330075db0f744d6d40f0323135f326368846325cc66975f8771260d14ba5cd96156c36e134e9bf500df4bff924d6bdcc9ef82
7
- data.tar.gz: 5a1818958e13bac846e04616c1762f1fdbb3d7e5e664d8d7c56e86b36e1fa10d608bf242bd666e43ba1bea5eef6f3a17be01e2c1f715bab205413aabe3a764eb
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
@@ -17,3 +17,4 @@ spec/reports
17
17
  test/tmp
18
18
  test/version_tmp
19
19
  tmp
20
+ gemfiles/
data/.rubocop.yml ADDED
@@ -0,0 +1,58 @@
1
+ AllCops:
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
12
+ Exclude:
13
+ - 'gemfiles/**/*'
14
+
15
+ Naming/MethodParameterName:
16
+ Enabled: false
17
+
18
+ Metrics/BlockLength:
19
+ Exclude:
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'
28
+
29
+ Naming/FileName:
30
+ Exclude:
31
+ - 'lib/active_remote-cached.rb'
32
+
33
+
34
+ Metrics/ModuleLength:
35
+ Exclude:
36
+ - 'lib/active_remote/cached.rb' # this module is big
37
+
38
+ Style/Documentation:
39
+ Enabled: false
40
+
41
+ # The generated finder methods in this file are long heredocs.
42
+ Metrics/MethodLength:
43
+ Exclude:
44
+ - 'lib/active_remote/cached.rb'
45
+ - 'spec/**/*'
46
+
47
+ Style/HashSyntax:
48
+ Description: >-
49
+ Prefer Ruby 1.8 hash syntax { :a => 1, :b => 2 }
50
+ over 1.9 syntax { a: 1, b: 2 }.
51
+ StyleGuide: 'https://git.moneydesktop.com/dev/ruby-style-guide#hash-literals'
52
+ EnforcedStyle: hash_rockets
53
+ Exclude:
54
+ - 'Gemfile'
55
+
56
+ # Use lambdas instead of stabbys
57
+ Style/Lambda:
58
+ EnforcedStyle: lambda
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/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in active_remote-cached.gemspec
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # ActiveRemote::Cached
2
2
 
3
+ [![CI](https://github.com/skunkworker/active_remote-cached/actions/workflows/ci.yml/badge.svg)](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,12 +1,9 @@
1
- require "bundler/gem_tasks"
2
- require 'rake/testtask'
1
+ # frozen_string_literal: true
3
2
 
4
- Rake::TestTask.new do |t|
5
- t.libs.push "lib"
6
- t.libs.push "spec"
7
- t.pattern = "spec/**/*_spec.rb"
8
- t.verbose = true
9
- end
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'appraisal'
6
+
7
+ ::RSpec::Core::RakeTask.new(:spec)
10
8
 
11
- task :spec => :test
12
9
  task :default => :spec
@@ -1,27 +1,58 @@
1
- # -*- encoding: utf-8 -*-
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+
5
+ lib = File.expand_path('lib', __dir__)
3
6
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
7
  require 'active_remote/cached/version'
5
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
+
6
24
  Gem::Specification.new do |gem|
7
- gem.name = "active_remote-cached"
25
+ gem.name = 'active_remote-cached'
8
26
  gem.version = ActiveRemote::Cached::VERSION
9
- gem.authors = ["Brandon Dewitt", "MXDevExperience"]
10
- gem.email = ["brandonsdewitt@gmail.com", "devexperience@mx.com"]
11
- gem.description = %q{ Provides "cached" finders and a DSL to enumerate which finders should have cached versions }
12
- gem.summary = %q{ Provides a configuration for caching mechanisms and finders on ActiveRemote models that are cached/cacheable }
13
- gem.homepage = ""
14
-
15
- gem.files = `git ls-files`.split($/)
16
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
- gem.require_paths = ["lib"]
19
-
20
- gem.add_dependency "active_remote"
21
- gem.add_dependency "activesupport"
22
-
23
- gem.add_development_dependency "bundler"
24
- gem.add_development_dependency "mocha"
25
- gem.add_development_dependency "pry"
26
- gem.add_development_dependency "rake"
27
+ gem.authors = ['Brandon Dewitt', 'MXDevExperience']
28
+ gem.email = ['brandonsdewitt@gmail.com', 'devexperience@mx.com']
29
+ gem.description = ' Provides "cached" finders and a DSL to enumerate which finders should have cached versions '
30
+ gem.summary = ' Provides a configuration for caching mechanisms and finders on ActiveRemote models'
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
+ }
39
+
40
+ gem.required_ruby_version = '>= 3.1'
41
+ gem.files = gem_files
42
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
43
+ gem.require_paths = ['lib']
44
+
45
+ gem.add_dependency 'active_remote', '>= 6.1'
46
+ # NullStore and ActiveSupport::VERSION::STRING. Matches the active_remote floor.
47
+ gem.add_dependency 'activesupport', '>= 6.1'
48
+
49
+ gem.add_development_dependency 'appraisal'
50
+ gem.add_development_dependency 'bundler'
51
+ gem.add_development_dependency 'mocha'
52
+ # ostruct leaves the default gems in ruby 4.0
53
+ gem.add_development_dependency 'ostruct'
54
+ gem.add_development_dependency 'pry'
55
+ gem.add_development_dependency 'rake'
56
+ gem.add_development_dependency 'rspec', '>= 3.0'
57
+ gem.add_development_dependency 'rubocop', '~> 1.80'
27
58
  end
@@ -1,57 +1,94 @@
1
- module ActiveRemote::Cached
2
- class ArgumentKeys
3
- attr_reader :arguments, :argument_string, :options
4
-
5
- REMOVE_CHARACTERS = /[[:space:]+=><{}\[\];:\-,]/
6
- REPLACE_MAP = [
7
- [" ", "SP"],
8
- ["+", "PL"],
9
- ["=", "EQ"],
10
- [">", "GT"],
11
- ["<", "LT"],
12
- ["{", "LB"],
13
- ["}", "RB"],
14
- ["[", "LB2"],
15
- ["]", "RB2"],
16
- [";", "SC"],
17
- [":", "CO"],
18
- ["-", "DA"],
19
- [",", "COM"],
20
- ].freeze
21
-
22
- def initialize(*arguments, options)
23
- @options = options
24
- @arguments = arguments.flatten.compact
25
- @argument_string = ""
26
-
27
- @arguments.each do |argument|
28
- @argument_string << "#{argument}"
29
- end
30
- end
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRemote
4
+ module Cached
5
+ class ArgumentKeys
6
+ attr_reader :arguments, :argument_string, :options
7
+
8
+ REMOVE_CHARACTERS = /[[:space:]+=><{}\[\];:\-,]/
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{[%./]}
31
40
 
32
- def cache_key
33
- return @argument_string.gsub(REMOVE_CHARACTERS, "") if remove_characters?
34
- if replace_characters?
35
- REPLACE_MAP.each do |character, replacement|
36
- @argument_string.gsub!(character, replacement)
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])}"
37
50
  end
51
+
52
+ new(pairs.join(PAIR_SEPARATOR), options)
38
53
  end
39
54
 
40
- @argument_string
41
- end
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
42
59
 
43
- def to_s
44
- cache_key
45
- end
60
+ def self.escape_value(value)
61
+ value.to_s.gsub(ESCAPE_CHARACTERS, ESCAPE_MAP)
62
+ end
63
+ private_class_method :escape_value
46
64
 
47
- private
65
+ def initialize(*arguments, options)
66
+ @options = options
67
+ @arguments = arguments.flatten.compact
68
+ @argument_string = @arguments.join
69
+ end
48
70
 
49
- def remove_characters?
50
- options.fetch(:active_remote_cached_remove_characters, false)
51
- end
71
+ def cache_key
72
+ return @argument_string.gsub(REMOVE_CHARACTERS, '') if remove_characters?
73
+ return @argument_string unless replace_characters?
74
+
75
+ # One pass, rather than one gsub for each entry in the map.
76
+ @argument_string.gsub(REPLACE_CHARACTERS, REPLACE_MAP)
77
+ end
78
+
79
+ def to_s
80
+ cache_key
81
+ end
82
+
83
+ private
52
84
 
53
- def replace_characters?
54
- options.fetch(:active_remote_cached_replace_characters, false)
85
+ def remove_characters?
86
+ options.fetch(:active_remote_cached_remove_characters, false)
87
+ end
88
+
89
+ def replace_characters?
90
+ options.fetch(:active_remote_cached_replace_characters, false)
91
+ end
55
92
  end
56
93
  end
57
94
  end