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/.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/.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 - 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/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. 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))