From 776487ede4c3102e564c657930c365859a0c2f0e Mon Sep 17 00:00:00 2001 From: Aaron Rosenberg Date: Thu, 26 Feb 2026 22:29:16 -0500 Subject: [PATCH 1/3] Update Ruby >= 3.3, RuboCop 1.85, and rubocop-rspec 3.9 - Bump required_ruby_version from >= 3.1.6 to >= 3.3 - Update TargetRubyVersion from 2.6 to 3.3 - Update RuboCop 1.65 -> 1.85 and rubocop-rspec 3.0 -> 3.9 - Migrate rubocop-rspec from require to plugins - Rename deprecated IgnoredPatterns to AllowedPatterns - Update all development dependencies and Bundler to latest - Add rake to Gemfile (no longer a default gem in newer Rubies) - Gitignore Ruby version manager tool files - Fix new cop offenses: Style/RedundantRegexpArgument, RSpec/BeEq, Style/HashSyntax, Layout/LineContinuationSpacing, Lint/RedundantCopDisableDirective Co-Authored-By: Claude Opus 4.6 --- .gitignore | 3 + .rubocop.yml | 7 +- Gemfile | 1 + Gemfile.lock | 102 +++++++++++------- cacheable.gemspec | 6 +- .../cache_adapters/memory_adapter.rb | 2 +- lib/cacheable/method_generator.rb | 4 +- spec/.rubocop.yml | 11 -- .../cache_adapters/memory_adapter_spec.rb | 4 +- spec/cacheable/cacheable_spec.rb | 15 +-- 10 files changed, 89 insertions(+), 66 deletions(-) diff --git a/.gitignore b/.gitignore index 331d5ee..814b349 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ # Used by dotenv library to load environment variables. .env .rvmrc +.ruby-version +.tool-versions +.mise.toml # Because macs .DS_Store diff --git a/.rubocop.yml b/.rubocop.yml index ee17407..79bbe90 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,12 +1,13 @@ -require: 'rubocop-rspec' +plugins: + - rubocop-rspec AllCops: NewCops: enable - TargetRubyVersion: 2.6 + TargetRubyVersion: 3.3 Layout/LineLength: Max: 120 - IgnoredPatterns: ['#.*'] + AllowedPatterns: ['#.*'] Layout/SpaceInsideHashLiteralBraces: EnforcedStyle: no_space diff --git a/Gemfile b/Gemfile index a745b6b..e65fb48 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ group :development, :test do gem 'guard-rspec' gem 'guard-rubocop' gem 'pry-byebug' + gem 'rake' gem 'rspec' gem 'rubocop' gem 'rubocop-rspec' diff --git a/Gemfile.lock b/Gemfile.lock index f26c881..cab1bbf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,17 +6,23 @@ PATH GEM remote: https://rubygems.org/ specs: - ast (2.4.2) - byebug (11.1.3) + addressable (2.8.8) + public_suffix (>= 2.0.2, < 8.0) + ast (2.4.3) + bigdecimal (4.0.1) + byebug (13.0.0) + reline (>= 0.6.0) coderay (1.1.3) - diff-lcs (1.5.1) - ffi (1.17.0-arm64-darwin) - ffi (1.17.0-x86_64-darwin) - ffi (1.17.0-x86_64-linux-gnu) - formatador (1.1.0) - guard (2.18.1) + diff-lcs (1.6.2) + ffi (1.17.3-arm64-darwin) + ffi (1.17.3-x86_64-darwin) + ffi (1.17.3-x86_64-linux-gnu) + formatador (1.2.3) + reline + guard (2.20.1) formatador (>= 0.2.4) listen (>= 2.7, < 4.0) + logger (~> 1.6) lumberjack (>= 1.0.12, < 2.0) nenv (~> 0.1) notiffany (~> 0.0) @@ -31,66 +37,85 @@ GEM guard-rubocop (1.5.0) guard (~> 2.0) rubocop (< 2.0) - json (2.7.2) - language_server-protocol (3.17.0.3) - listen (3.9.0) + io-console (0.8.2) + json (2.18.1) + json-schema (6.1.0) + addressable (~> 2.8) + bigdecimal (>= 3.1, < 5) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + listen (3.10.0) + logger rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - lumberjack (1.2.10) + logger (1.7.0) + lumberjack (1.4.2) + mcp (0.7.1) + json-schema (>= 4.1) method_source (1.1.0) nenv (0.3.0) notiffany (0.1.3) nenv (~> 0.1) shellany (~> 0.0) - parallel (1.25.1) - parser (3.3.4.0) + parallel (1.27.0) + parser (3.3.10.2) ast (~> 2.4.1) racc - pry (0.14.2) + prism (1.9.0) + pry (0.16.0) coderay (~> 1.1) method_source (~> 1.0) - pry-byebug (3.10.1) - byebug (~> 11.0) - pry (>= 0.13, < 0.15) + reline (>= 0.6.0) + pry-byebug (3.12.0) + byebug (~> 13.0) + pry (>= 0.13, < 0.17) + public_suffix (7.0.2) racc (1.8.1) rainbow (3.1.1) + rake (13.3.1) rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - regexp_parser (2.9.2) - rexml (3.4.4) - rspec (3.13.0) + regexp_parser (2.11.3) + reline (0.6.3) + io-console (~> 0.5) + rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.0) + rspec-core (3.13.6) rspec-support (~> 3.13.0) - rspec-expectations (3.13.1) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.1) + rspec-mocks (3.13.7) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-support (3.13.1) - rubocop (1.65.1) + rspec-support (3.13.7) + rubocop (1.85.0) json (~> 2.3) - language_server-protocol (>= 3.17.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + mcp (~> 0.6) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.4, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.3) - parser (>= 3.3.1.0) - rubocop-rspec (3.0.3) - rubocop (~> 1.61) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.0) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-rspec (3.9.0) + lint_roller (~> 1.1) + rubocop (~> 1.81) ruby-progressbar (1.13.0) shellany (0.0.1) - thor (1.3.1) - unicode-display_width (2.5.0) + thor (1.5.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) PLATFORMS arm64-darwin-23 @@ -104,9 +129,10 @@ DEPENDENCIES guard-rspec guard-rubocop pry-byebug + rake rspec rubocop rubocop-rspec BUNDLED WITH - 2.5.17 + 4.0.7 diff --git a/cacheable.gemspec b/cacheable.gemspec index e59dd47..57ad490 100644 --- a/cacheable.gemspec +++ b/cacheable.gemspec @@ -7,14 +7,14 @@ Gem::Specification.new do |s| s.name = 'cacheable' s.version = Cacheable::VERSION s.summary = 'Add caching to any Ruby method in a aspect orientated programming approach.' - s.description = 'Add caching simply without modifying your existing code. '\ - 'Includes configurable options for simple cache invalidation. '\ + s.description = 'Add caching simply without modifying your existing code. ' \ + 'Includes configurable options for simple cache invalidation. ' \ 'See README on github for more information.' s.authors = ['Jess Hottenstein', 'Ryan Laughlin', 'Aaron Rosenberg'] s.email = 'support@splitwise.com' s.files = Dir['lib/**/*', 'README.md', 'cache-adapters.md'] s.homepage = 'https://github.com/splitwise/cacheable' s.licenses = 'MIT' - s.required_ruby_version = '>= 3.1.6' + s.required_ruby_version = '>= 3.3' s.metadata = {'rubygems_mfa_required' => 'true'} end diff --git a/lib/cacheable/cache_adapters/memory_adapter.rb b/lib/cacheable/cache_adapters/memory_adapter.rb index 917b6d4..60129b3 100644 --- a/lib/cacheable/cache_adapters/memory_adapter.rb +++ b/lib/cacheable/cache_adapters/memory_adapter.rb @@ -23,7 +23,7 @@ def fetch(key, _options = {}) write(key, yield) end - def delete(key) + def delete(key) # rubocop:disable Naming/PredicateMethod -- mimics the ActiveSupport::Cache::Store#delete interface and isn't a predicate return false unless exist?(key) cache.delete key diff --git a/lib/cacheable/method_generator.rb b/lib/cacheable/method_generator.rb index 95e7625..c2b47d8 100644 --- a/lib/cacheable/method_generator.rb +++ b/lib/cacheable/method_generator.rb @@ -11,7 +11,7 @@ def cacheable(*original_method_names, **opts) private def method_interceptor_module_name - class_name = name&.gsub(/:/, '') || to_s.gsub(/[^a-zA-Z_0-9]/, '') + class_name = name&.gsub(':', '') || to_s.gsub(/[^a-zA-Z_0-9]/, '') "#{class_name}Cacher" end @@ -35,7 +35,7 @@ def create_cacheable_methods(original_method_name, opts = {}) end define_method(method_names[:with_cache_method_name]) do |*args| - Cacheable.cache_adapter.fetch(__send__(method_names[:key_format_method_name], *args), opts[:cache_options]) do + Cacheable.cache_adapter.fetch(__send__(method_names[:key_format_method_name], *args), opts[:cache_options]) do # rubocop:disable Lint/UselessDefaultValueArgument -- not Hash#fetch; second arg is cache options (e.g. expires_in) passed to the adapter __send__(method_names[:without_cache_method_name], *args) end end diff --git a/spec/.rubocop.yml b/spec/.rubocop.yml index 3ec7c25..8481f99 100644 --- a/spec/.rubocop.yml +++ b/spec/.rubocop.yml @@ -3,23 +3,12 @@ inherit_from: ../.rubocop.yml Layout/LineLength: Max: 170 -# This cop does not like rspec dsl syntax https://github.com/bbatsov/rubocop/pull/4237#issuecomment-291408032 -Lint/AmbiguousBlockAssociation: - Enabled: false - Metrics/BlockLength: Enabled: false -RSpec/AnyInstance: - Enabled: false - RSpec/ExampleLength: Max: 100 -# Disabling the ExpectInHook cop can make testing similar preconditions with only slightly test differences easier -RSpec/ExpectInHook: - Enabled: false - RSpec/MessageSpies: Enabled: false diff --git a/spec/cacheable/cache_adapters/memory_adapter_spec.rb b/spec/cacheable/cache_adapters/memory_adapter_spec.rb index 0a10a99..7730829 100644 --- a/spec/cacheable/cache_adapters/memory_adapter_spec.rb +++ b/spec/cacheable/cache_adapters/memory_adapter_spec.rb @@ -20,12 +20,12 @@ describe '#delete' do it 'returns false if the value was not found' do - expect(cache.delete(key)).to eq(false) + expect(cache.delete(key)).to be(false) end it 'returns true if the value was found' do cache.fetch(key) { true } - expect(cache.delete(key)).to eq(true) + expect(cache.delete(key)).to be(true) end it 'removes the value from the cache' do diff --git a/spec/cacheable/cacheable_spec.rb b/spec/cacheable/cacheable_spec.rb index 084970a..85a8cfe 100644 --- a/spec/cacheable/cacheable_spec.rb +++ b/spec/cacheable/cacheable_spec.rb @@ -8,8 +8,11 @@ let(:class_definition) do cacheable_method_name = cacheable_method cacheable_method_inner_name = cacheable_method_inner + # Capture described_class here because class_exec changes self to + # the anonymous class, where the RSpec helper is not available. + mod = described_class proc do - include Cacheable # rubocop:disable RSpec/DescribedClass + include mod define_method(cacheable_method_name) do |arg = nil| send cacheable_method_inner_name, arg @@ -65,8 +68,8 @@ cacheable_object.send(cacheable_method) expect { cacheable_object.send("clear_#{cacheable_method}_cache") } - .to change { described_class.cache_adapter.read(cacheable_object.cacheable_method_key_format) }.to(nil) - .and not_change { described_class.cache_adapter.read(any_other_cached_value) } # rubocop:disable Layout/MultilineMethodCallIndentation + .to change { described_class.cache_adapter.read(cacheable_object.cacheable_method_key_format) }.to(nil) # rubocop:disable Lint/AmbiguousBlockAssociation + .and not_change { described_class.cache_adapter.read(any_other_cached_value) } end it 'allows access to `super` via a module interceptor' do @@ -230,7 +233,7 @@ attr_accessor :secret - cacheable custom_key_object_access_cacheable_method, key_format: proc { |c| c.secret } + cacheable custom_key_object_access_cacheable_method, key_format: proc { |obj, _method_name, _args| obj.secret } end cacheable_object.secret = 'some_state_on_the_object' @@ -376,7 +379,7 @@ cacheable symbol_unless_cache_method, unless: :cache_control_method def cache_control_method(*_args) - true + 'a truthy value skips caching with :unless' end end @@ -455,7 +458,7 @@ def cache_control_method calculate_hard_value end - cacheable :cache_method_with_cache_options, cache_options: cache_options + cacheable :cache_method_with_cache_options, cache_options: end expect(described_class.cache_adapter).to receive(:fetch).with(anything, hash_including(cache_options)) From fedc7b9787bf2f4c46af744328e265e5065eaaca Mon Sep 17 00:00:00 2001 From: Aaron Rosenberg Date: Thu, 26 Feb 2026 22:50:36 -0500 Subject: [PATCH 2/3] Replace Travis CI with GitHub Actions Travis CI stopped free OSS builds in 2021. Add a GitHub Actions workflow that tests Ruby 3.3, 3.4, and 4.0 on every push to main and on all pull requests. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 27 +++++++++++++++++++++++++++ .travis.yml | 6 ------ 2 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9b4c21a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + ruby: ['3.3', '3.4', '4.0'] + + name: Ruby ${{ matrix.ruby }} + + steps: + - uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - run: bundle exec rake diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index bdf3654..0000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -language: ruby -cache: bundler -script: - - bundle exec rspec - - bundle exec rubocop - From 8fa5a13179b9da35cdd6d6d414a7c0c46e8533b5 Mon Sep 17 00:00:00 2001 From: Aaron Rosenberg Date: Thu, 26 Feb 2026 22:56:30 -0500 Subject: [PATCH 3/3] Fix README typo and add CI badge and Ruby version Co-Authored-By: Claude Opus 4.6 --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ce04371..a44e9f0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # Cacheable +[![CI](https://github.com/splitwise/cacheable/actions/workflows/ci.yml/badge.svg)](https://github.com/splitwise/cacheable/actions/workflows/ci.yml) + By [Splitwise](https://www.splitwise.com) +Requires Ruby >= 3.3 + Cacheable is a gem which adds method caching in Ruby following an [aspect-oriented programming (AOP)](https://en.wikipedia.org/wiki/Aspect-oriented_programming) paradigm. Its core goals are: * ease of use (method annotation) @@ -131,7 +135,7 @@ Fetching data from GitHub #### Default -By default, Cacheable will construct key a key in the format `[cache_key || class_name, method_name]` without using method arguments. +By default, Cacheable will construct a key in the format `[cache_key || class_name, method_name]` without using method arguments. 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.