pax_global_header00006660000000000000000000000064151516544370014525gustar00rootroot0000000000000052 comment=18332b96066c2bb57cfa10dd8227539a8a9e740d dkubb-memoizable-0a01570/000077500000000000000000000000001515165443700151755ustar00rootroot00000000000000dkubb-memoizable-0a01570/.github/000077500000000000000000000000001515165443700165355ustar00rootroot00000000000000dkubb-memoizable-0a01570/.github/workflows/000077500000000000000000000000001515165443700205725ustar00rootroot00000000000000dkubb-memoizable-0a01570/.github/workflows/docs.yml000066400000000000000000000004701515165443700222460ustar00rootroot00000000000000name: Docs on: [push, pull_request] jobs: docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: '3.4' bundler-cache: true - name: Generate and verify documentation run: bundle exec rake docs dkubb-memoizable-0a01570/.github/workflows/lint.yml000066400000000000000000000004421515165443700222630ustar00rootroot00000000000000name: Lint on: [push, pull_request] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: '3.4' bundler-cache: true - name: Run linters run: bundle exec rake lint dkubb-memoizable-0a01570/.github/workflows/mutant.yml000066400000000000000000000005161515165443700226270ustar00rootroot00000000000000name: Mutant on: [push, pull_request] jobs: mutant: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: ruby/setup-ruby@v1 with: ruby-version: '3.4' bundler-cache: true - name: Run mutant run: bundle exec rake mutant dkubb-memoizable-0a01570/.github/workflows/push.yml000066400000000000000000000017331515165443700223000ustar00rootroot00000000000000name: Push gem to RubyGems on: push: tags: - "v*" permissions: contents: read jobs: push: if: github.repository == 'dkubb/memoizable' runs-on: ubuntu-latest environment: name: rubygems.org url: https://rubygems.org/gems/memoizable permissions: contents: write id-token: write steps: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: ruby bundler-cache: true - uses: rubygems/configure-rubygems-credentials@v1.0.0 - name: Update RubyGems run: gem update --system - name: Build gem run: bundle exec rake clobber build - name: Sign gem with Sigstore run: gem exec sigstore-cli sign pkg/*.gem --bundle pkg/memoizable.gem.sigstore.json - name: Push gem run: gem push pkg/*.gem --attestation pkg/memoizable.gem.sigstore.json - name: Wait for release run: gem exec rubygems-await pkg/*.gem dkubb-memoizable-0a01570/.github/workflows/steep.yml000066400000000000000000000004601515165443700224350ustar00rootroot00000000000000name: Steep on: [push, pull_request] jobs: steep: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: '3.4' bundler-cache: true - name: Run steep type checker run: bundle exec rake steep dkubb-memoizable-0a01570/.github/workflows/test.yml000066400000000000000000000006511515165443700222760ustar00rootroot00000000000000name: Test on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: ruby-version: ['3.2', '3.3', '3.4', '4.0', 'jruby-10.0'] steps: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true - name: Run tests run: bundle exec rake test dkubb-memoizable-0a01570/.gitignore000066400000000000000000000000701515165443700171620ustar00rootroot00000000000000.yardoc/ Gemfile.lock coverage/ doc/ measurements/ pkg/ dkubb-memoizable-0a01570/.jrubyrc000066400000000000000000000000251515165443700166530ustar00rootroot00000000000000debug.fullTrace=true dkubb-memoizable-0a01570/.mutant.yml000066400000000000000000000002341515165443700173050ustar00rootroot00000000000000--- usage: opensource integration: name: rspec requires: - memoizable matcher: subjects: - Memoizable* mutation: operators: full timeout: 1.0 dkubb-memoizable-0a01570/.rspec000066400000000000000000000000701515165443700163070ustar00rootroot00000000000000--backtrace --format progress --order random --warnings dkubb-memoizable-0a01570/.rubocop.yml000066400000000000000000000017541515165443700174560ustar00rootroot00000000000000plugins: - rubocop-performance - rubocop-rake - rubocop-rspec AllCops: NewCops: enable TargetRubyVersion: 3.2 Style/StringLiterals: EnforcedStyle: double_quotes Style/StringLiteralsInInterpolation: EnforcedStyle: double_quotes # Disabled to be consistent with Standard Ruby Style/RescueStandardError: Enabled: false # Disabled to be consistent with Standard Ruby Style/EmptyMethod: Enabled: false # Disabled to be consistent with Standard Ruby Layout/ArgumentAlignment: Enabled: false # Disabled to be consistent with Standard Ruby Layout/SpaceInsideHashLiteralBraces: Enabled: false # Disabled because the string references are intentional (the classes don't exist) RSpec/VerifiedDoubleReference: Enabled: false # Only disabled for the serializable spec where Marshal.load is legitimately tested Security/MarshalLoad: Exclude: - "spec/integration/serializable_spec.rb" # Allow more memoized helpers in complex test setups RSpec/MultipleMemoizedHelpers: Max: 10 dkubb-memoizable-0a01570/.standard.yml000066400000000000000000000000651515165443700175770ustar00rootroot00000000000000plugins: - standard-performance ruby_version: 3.1 dkubb-memoizable-0a01570/.yardopts000066400000000000000000000001001515165443700170320ustar00rootroot00000000000000lib/**/*.rb - CHANGELOG.md CONTRIBUTING.md LICENSE.md README.md dkubb-memoizable-0a01570/CHANGELOG.md000066400000000000000000000062441515165443700170140ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.5.1] - 2026-03-03 ### Fixed - Include RBS signature file in gem package ## [0.5.0] - 2026-02-09 ### Added - `Memory#delete` method to remove a specific memoized value from cache - `Memory#clear` method to remove all memoized values from cache - `Memory#fetch` now accepts an optional default argument - Steep for static type checking - Mutant for mutation testing - RuboCop and Standard Ruby for linting - YARD and Yardstick for documentation coverage - GitHub Actions CI pipeline (replacing Travis CI) - GitHub Actions workflow for publishing gems to RubyGems with Sigstore attestation ### Changed - Renamed `Memory#[]=` to `Memory#store` - `ModuleMethods#memoize` now raises `ArgumentError` when method is already memoized - Memoization cache now uses composite keys `[class, method_name]` to support inheritance ([#13](https://github.com/dkubb/memoizable/issues/13)) - Replaced `thread_safe` gem dependency with Ruby's built-in `Monitor` ### Removed - `Memory#key?` method - `ModuleMethods#memoized?` method - `ModuleMethods#included` method - `thread_safe` gem dependency ## [0.4.2] - 2014-03-27 ### Changed - Updated `thread_safe` dependency to use semantic versioning compatible version ## [0.4.1] - 2014-03-04 ### Added - Support for Ruby 2.1.0 ### Changed - Updated `thread_safe` dependency to ~> 0.2.0 ## [0.4.0] - 2013-12-24 ### Added - `Memory#marshal_dump` and `Memory#marshal_load` methods for Marshal serialization support ([#10](https://github.com/dkubb/memoizable/issues/10)) ## [0.3.1] - 2013-12-18 ### Changed - Added double-checked locking to `Memory#fetch` for improved thread safety ### Removed - Unnecessary `Memory#set` method ## [0.3.0] - 2013-12-15 ### Added - Thread-safe memory operations using `thread_safe` gem - `BlockNotAllowedError` raised when passing a block to memoized methods - `ModuleMethods#included` to allow module methods to be memoized ### Changed - Memory is now shallowly frozen after initialization ## [0.2.0] - 2013-11-18 ### Added - Core memoization functionality - `Memoizable::MethodBuilder` for memoized method creation - `InstanceMethods#memoize` to manually set memoized values - `InstanceMethods#freeze` support for frozen objects - `ModuleMethods#unmemoized_instance_method` to access original method - Thread-safe cache using `ThreadSafe::Cache` [Unreleased]: https://github.com/dkubb/memoizable/compare/v0.5.1...HEAD [0.5.1]: https://github.com/dkubb/memoizable/compare/v0.5.0...v0.5.1 [0.5.0]: https://github.com/dkubb/memoizable/compare/v0.4.2...v0.5.0 [0.4.2]: https://github.com/dkubb/memoizable/compare/v0.4.1...v0.4.2 [0.4.1]: https://github.com/dkubb/memoizable/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/dkubb/memoizable/compare/v0.3.1...v0.4.0 [0.3.1]: https://github.com/dkubb/memoizable/compare/v0.3.0...v0.3.1 [0.3.0]: https://github.com/dkubb/memoizable/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/dkubb/memoizable/compare/v0.0.0...v0.2.0 dkubb-memoizable-0a01570/CONTRIBUTING.md000066400000000000000000000020241515165443700174240ustar00rootroot00000000000000Contributing ------------ * If you want your code merged into the mainline, please discuss the proposed changes with me before doing any work on it. This library is still in early development, and the direction it is going may not always be clear. Some features may not be appropriate yet, may need to be deferred until later when the foundation for them is laid, or may be more applicable in a plugin. * Fork the project. * Make your feature addition or bug fix. * Follow this [style guide](https://github.com/dkubb/styleguide). * Add specs for it. This is important so I don't break it in a future version unintentionally. Tests must cover all branches within the code, and code must be fully covered. * Commit, do not mess with Rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) * Run "rake ci". This must pass and not show any regressions in the metrics for the code to be merged. * Send me a pull request. Bonus points for topic branches. dkubb-memoizable-0a01570/Gemfile000066400000000000000000000007621515165443700164750ustar00rootroot00000000000000# frozen_string_literal: true source "https://rubygems.org" gemspec group :test do gem "mutant-rspec", ">= 0.14" gem "rake", ">= 13.3.1" gem "rspec", ">= 3.13.2" gem "rubocop", ">= 1.74" gem "rubocop-performance", ">= 1.24" gem "rubocop-rake", ">= 0.7" gem "rubocop-rspec", ">= 3.5" gem "simplecov", ">= 0.22" gem "standard", ">= 1.46" gem "standard-performance", ">= 1.7" gem "steep", ">= 1.9", platforms: :ruby gem "yard", ">= 0.9.38" gem "yardstick", ">= 0.9.9" end dkubb-memoizable-0a01570/LICENSE.md000066400000000000000000000020561515165443700166040ustar00rootroot00000000000000Copyright (c) 2013-2026 Dan Kubb, Erik Berlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. dkubb-memoizable-0a01570/README.md000066400000000000000000000057751515165443700164720ustar00rootroot00000000000000# Memoizable [![Gem Version](http://img.shields.io/gem/v/memoizable.svg)][gem] [![Test](https://github.com/dkubb/memoizable/actions/workflows/test.yml/badge.svg)][test] [![Lint](https://github.com/dkubb/memoizable/actions/workflows/lint.yml/badge.svg)][lint] [![Mutant](https://github.com/dkubb/memoizable/actions/workflows/mutant.yml/badge.svg)][mutant] [![Docs](https://github.com/dkubb/memoizable/actions/workflows/docs.yml/badge.svg)][docs] [![Steep](https://github.com/dkubb/memoizable/actions/workflows/steep.yml/badge.svg)][steep] [gem]: https://rubygems.org/gems/memoizable [test]: https://github.com/dkubb/memoizable/actions/workflows/test.yml [lint]: https://github.com/dkubb/memoizable/actions/workflows/lint.yml [mutant]: https://github.com/dkubb/memoizable/actions/workflows/mutant.yml [docs]: https://github.com/dkubb/memoizable/actions/workflows/docs.yml [steep]: https://github.com/dkubb/memoizable/actions/workflows/steep.yml Memoize method return values ## Changelog See [CHANGELOG.md](CHANGELOG.md) for details. ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for details. ## Rationale Memoization is an optimization that saves the return value of a method so it doesn't need to be re-computed every time that method is called. For example, perhaps you've written a method like this: ```ruby class Planet # This is the equation for the area of a sphere. If it's true for a # particular instance of a planet, then that planet is spherical. def spherical? 4 * Math::PI * radius ** 2 == area end end ``` This code will re-compute whether a particular planet is spherical every time the method is called. If the method is called more than once, it may be more efficient to save the computed value in an instance variable, like so: ```ruby class Planet def spherical? @spherical ||= 4 * Math::PI * radius ** 2 == area end end ``` One problem with this approach is that, if the return value is `false`, the value will still be computed each time the method is called. It also becomes unweildy for methods that grow to be longer than one line. These problems can be solved by mixing-in the `Memoizable` module and memoizing the method. ```ruby require 'memoizable' class Planet include Memoizable def spherical? 4 * Math::PI * radius ** 2 == area end memoize :spherical? end ``` ## Warning The example above assumes that the radius and area of a planet will not change over time. This seems like a reasonable assumption but such an assumption is not safe in every domain. If it was possible for one of the attributes to change between method calls, memoizing that value could produce the wrong result. Please keep this in mind when considering which methods to memoize. Supported Ruby Versions ----------------------- This library aims to support and is tested against the following Ruby versions: * Ruby 3.2 * Ruby 3.3 * Ruby 3.4 * Ruby 4.0 If something doesn't work on one of these versions, it's a bug. ## Copyright Copyright © 2013-2026 Dan Kubb, Erik Berlin. See LICENSE for details. dkubb-memoizable-0a01570/Rakefile000066400000000000000000000024621515165443700166460ustar00rootroot00000000000000# frozen_string_literal: true require "bundler/gem_tasks" # Override release task to skip gem push (handled by GitHub Actions with attestations) Rake::Task["release"].clear desc "Build gem and create tag (gem push handled by CI)" task release: %w[build release:guard_clean release:source_control_push] require "rspec/core/rake_task" require "rubocop/rake_task" require "standard/rake" require "yard" require "yardstick/rake/measurement" require "yardstick/rake/verify" RSpec::Core::RakeTask.new(:spec) RuboCop::RakeTask.new(:rubocop) YARD::Rake::YardocTask.new(:yard) Yardstick::Rake::Measurement.new(:yardstick) Yardstick::Rake::Verify.new(:verify_measurements) do |verify| verify.threshold = 100 end desc "Run RuboCop and Standard Ruby" task lint: %i[rubocop standard] desc "Run RSpec" task test: :spec desc "Run Mutant" task :mutant do if Process.respond_to?(:fork) sh "bundle exec mutant run" else warn "Mutant is disabled (requires fork)" end end STEEP_AVAILABLE = begin require "steep" true rescue LoadError false end desc "Run Steep type checker" task :steep do if STEEP_AVAILABLE sh "bundle exec steep check" else warn "Steep is disabled" end end desc "Generate and verify documentation" task docs: %i[yard verify_measurements] task default: %i[test lint mutant docs steep] dkubb-memoizable-0a01570/Steepfile000066400000000000000000000002461515165443700170420ustar00rootroot00000000000000# frozen_string_literal: true target :lib do signature "sig" check "lib" library "monitor" configure_code_diagnostics(Steep::Diagnostic::Ruby.strict) end dkubb-memoizable-0a01570/lib/000077500000000000000000000000001515165443700157435ustar00rootroot00000000000000dkubb-memoizable-0a01570/lib/memoizable.rb000066400000000000000000000012701515165443700204140ustar00rootroot00000000000000# frozen_string_literal: true require "monitor" require "memoizable/instance_methods" require "memoizable/method_builder" require "memoizable/module_methods" require "memoizable/memory" require "memoizable/version" # Allow methods to be memoized module Memoizable include Memoizable::InstanceMethods # Default freezer Freezer = ->(value) { value.freeze }.freeze # rubocop:disable Style/SymbolProc # Hook called when module is included # # @param [Module] descendant # the module or class including Memoizable # # @return [self] # # @api private def self.included(descendant) super descendant.extend(ModuleMethods) end private_class_method :included end dkubb-memoizable-0a01570/lib/memoizable/000077500000000000000000000000001515165443700200675ustar00rootroot00000000000000dkubb-memoizable-0a01570/lib/memoizable/instance_methods.rb000066400000000000000000000017001515165443700237410ustar00rootroot00000000000000# frozen_string_literal: true module Memoizable # Methods mixed in to memoizable instances module InstanceMethods # Freeze the object # # @example # object.freeze # object is now frozen # # @return [Object] # # @api public def freeze memoized_method_cache # initialize method cache super end # Sets a memoized value for a method # # @example # object.memoize(hash: 12345) # # @param [Hash{Symbol => Object}] data # the data to memoize # # @return [self] # # @api public def memoize(data) data.each { |name, value| memoized_method_cache.store([self.class, name], value) } self end private # The memoized method results # # @return [Hash] # # @api private def memoized_method_cache @_memoized_method_cache ||= Memory.new({}) # rubocop:disable Naming/MemoizedInstanceVariableName end end end dkubb-memoizable-0a01570/lib/memoizable/memory.rb000066400000000000000000000075671515165443700217430ustar00rootroot00000000000000# frozen_string_literal: true module Memoizable # Storage for memoized methods class Memory # Initialize the memory storage for memoized methods # # @param [Hash] memory # # @return [undefined] # # @api private def initialize(memory) @memory = memory @monitor = Monitor.new freeze end # Get the value from memory # # @example # # memory = Memoizable::Memory.new(foo: 1) # memory[:foo] # => 1 # # @param [Symbol] name # # @return [Object] # # @api public def [](name) fetch(name) do raise NameError, "No method #{name} is memoized" end end # Store the value in memory # # @example # memory = Memoizable::Memory.new(foo: 1) # memory.store(:foo, 2) # memory[:foo] # => 2 # # @param [Symbol] name # @param [Object] value # # @return [undefined] # # @api public def store(name, value) @monitor.synchronize do raise ArgumentError, "The method #{name} is already memoized" if @memory.key?(name) @memory[name] = value end end # Fetch the value from memory, or store it if it does not exist # # @example # memory = Memoizable::Memory.new(foo: 1) # memory.fetch(:foo) { 2 } # => 1 # memory.fetch(:bar) { 2 } # => 2 # memory[:bar] # => 2 # memory.fetch(:baz, 3) # => 3 # # @param [Symbol] name # @param [Object] default # optional default value to return if the key is not found # # @yieldreturn [Object] # the value to memoize # # @return [Object] # # @api public def fetch(name, default = (no_default = true), &block) @memory.fetch(name) do # check for the key @monitor.synchronize do # acquire a lock if the key is not found @memory.fetch(name) do # recheck under lock @memory[name] = resolve_fetch_value(name, no_default, default, &block) end end end end private # Resolve the value for a fetch operation # # @param [Symbol] name # @param [Boolean] no_default # @param [Object] default # # @yieldreturn [Object] # # @return [Object] # # @api private def resolve_fetch_value(name, no_default, default) if block_given? yield elsif no_default raise KeyError, "key not found: #{name.inspect}" else default end end public # Remove a specific value from memory # # @example # memory = Memoizable::Memory.new(foo: 1) # memory.delete(:foo) # # @param [Symbol] name # # @return [Object] # # @api public def delete(name) @monitor.synchronize do @memory.delete(name) end end # Remove all values from memory # # @example # memory = Memoizable::Memory.new(foo: 1) # memory.clear # => memory # # @return [self] # # @api public def clear @monitor.synchronize do @memory.clear end self end # A hook that allows Marshal to dump the object # # @example # memory = Memoizable::Memory.new(foo: 1) # Marshal.dump(memory) # # => "\x04\bU:\x17Memoizable::Memory{\x06:\bfooi\x06" # # @return [Hash] # A hash used to populate the internal memory # # @api public def marshal_dump @memory end # A hook that allows Marshal to load the object # # @example # memory = Memoizable::Memory.new(foo: 1) # Marshal.load(Marshal.dump(memory)) # # => #1}> # # @param [Hash] hash # A hash used to populate the internal memory # # @return [undefined] # # @api public def marshal_load(hash) initialize(hash) end end end dkubb-memoizable-0a01570/lib/memoizable/method_builder.rb000066400000000000000000000067301515165443700234100ustar00rootroot00000000000000# frozen_string_literal: true module Memoizable # Build the memoized method class MethodBuilder # Raised when the method arity is invalid class InvalidArityError < ArgumentError # Initialize an invalid arity exception # # @param [Module] descendant # @param [Symbol] method # @param [Integer] arity # # @api private def initialize(descendant, method, arity) super("Cannot memoize #{descendant}##{method}, its arity is #{arity}") end end # Raised when a block is passed to a memoized method class BlockNotAllowedError < ArgumentError # Initialize a block not allowed exception # # @param [Module] descendant # @param [Symbol] method # # @api private def initialize(descendant, method) super("Cannot pass a block to #{descendant}##{method}, it is memoized") end end # The original method before memoization # # @example # method_builder.original_method # => :foo # # @return [UnboundMethod] # # @api public attr_reader :original_method # Initialize an object to build a memoized method # # @param [Module] descendant # @param [Symbol] method_name # @param [#call] freezer # # @return [undefined] # # @api private def initialize(descendant, method_name, freezer) @descendant = descendant @method_name = method_name @freezer = freezer @original_visibility = visibility @original_method = descendant.instance_method(@method_name) assert_arity(original_method.arity) end # Build a new memoized method # # @example # method_builder.call # => creates new method # # @return [MethodBuilder] # # @api public def call remove_original_method create_memoized_method set_method_visibility self end private # Assert the method arity is zero # # @param [Integer] arity # # @return [undefined] # # @raise [InvalidArityError] # # @api private def assert_arity(arity) return unless arity.nonzero? raise InvalidArityError.new(@descendant, @method_name, arity) end # Remove the original method # # @return [undefined] # # @api private def remove_original_method name = @method_name @descendant.module_eval { undef_method(name) } end # Create a new memoized method # # @return [undefined] # # @api private def create_memoized_method name = @method_name method = @original_method freezer = @freezer descendant = @descendant @descendant.define_method(name) do |&block| raise BlockNotAllowedError.new(self.class, name) if block # steep:ignore NoMethod memoized_method_cache.fetch([descendant, name]) do # steep:ignore NoMethod freezer.call(method.bind_call(self)) end end end # Set the memoized method visibility to match the original method # # @return [undefined] # # @api private def set_method_visibility @descendant.__send__(@original_visibility, @method_name) end # Get the visibility of the original method # # @return [Symbol] # # @api private def visibility if @descendant.private_method_defined?(@method_name) then :private elsif @descendant.protected_method_defined?(@method_name) then :protected else :public end end end end dkubb-memoizable-0a01570/lib/memoizable/module_methods.rb000066400000000000000000000033231515165443700234250ustar00rootroot00000000000000# frozen_string_literal: true module Memoizable # Methods mixed in to memoizable singleton classes module ModuleMethods include Memoizable # Return default deep freezer # # @return [#call] # # @api private def freezer Freezer end # Memoize a list of methods # # @example # memoize :hash # # @param [Array] methods # a list of methods to memoize # # @return [self] # # @api public def memoize(*methods) methods.each { |method| memoize_method(method) } self end # Return unmemoized instance method # # @example # # class Foo # include Memoizable # # def bar # end # memoize :bar # end # # Foo.unmemoized_instance_method(:bar) # # @param [Symbol] name # # @return [UnboundMethod] # the memoized method # # @raise [NameError] # raised if the method is unknown # # @api public def unmemoized_instance_method(name) memoized_methods[name].original_method end private # Memoize the named method # # @param [Symbol] method_name # a method name to memoize # # @return [undefined] # # @api private def memoize_method(method_name) raise ArgumentError, "The method #{method_name} is already memoized" if memoized_methods.key?(method_name) memoized_methods[method_name] = MethodBuilder.new( self, method_name, freezer ).call end # Return method builder registry # # @return [Hash] # # @api private def memoized_methods @memoized_methods ||= {} end end end dkubb-memoizable-0a01570/lib/memoizable/version.rb000066400000000000000000000001311515165443700220740ustar00rootroot00000000000000# frozen_string_literal: true module Memoizable # Gem version VERSION = "0.5.1" end dkubb-memoizable-0a01570/memoizable.gemspec000066400000000000000000000016671515165443700207000ustar00rootroot00000000000000# frozen_string_literal: true lib = File.expand_path("lib", __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "memoizable/version" Gem::Specification.new do |gem| gem.name = "memoizable" gem.version = Memoizable::VERSION gem.authors = ["Dan Kubb", "Erik Berlin"] gem.email = ["dan.kubb@gmail.com", "sferik@gmail.com"] gem.summary = "Memoize method return values" gem.description = gem.summary gem.homepage = "https://github.com/dkubb/memoizable" gem.license = "MIT" gem.required_ruby_version = ">= 3.2" gem.files = Dir["lib/**/*", "sig/**/*", "CHANGELOG.md", "CONTRIBUTING.md", "LICENSE.md", "README.md"] gem.require_paths = ["lib"] gem.metadata["homepage_uri"] = gem.homepage gem.metadata["source_code_uri"] = "https://github.com/dkubb/memoizable" gem.metadata["changelog_uri"] = "https://github.com/dkubb/memoizable/blob/main/CHANGELOG.md" gem.metadata["rubygems_mfa_required"] = "true" end dkubb-memoizable-0a01570/sig/000077500000000000000000000000001515165443700157575ustar00rootroot00000000000000dkubb-memoizable-0a01570/sig/memoizable.rbs000066400000000000000000000072771515165443700206300ustar00rootroot00000000000000# Type definitions for Memoizable module Memoizable VERSION: String # Cache key type: [defining_class, method_name] type cache_key = [Module, Symbol] include InstanceMethods # Default freezer that freezes objects Freezer: untyped # Hook called when module is included def self.included: (Module descendant) -> void # Methods mixed in to memoizable instances module InstanceMethods @_memoized_method_cache: Memory? # Freeze the object def freeze: () -> self # Sets memoized values for methods def memoize: (Hash[Symbol, untyped] data) -> self private # The memoized method results def memoized_method_cache: () -> Memory end # Methods mixed in to memoizable singleton classes module ModuleMethods : Module include Memoizable @memoized_methods: Hash[Symbol, MethodBuilder]? # Return default deep freezer def freezer: () -> untyped # Memoize a list of methods def memoize: (*Symbol methods) -> self # Return unmemoized instance method def unmemoized_instance_method: (Symbol name) -> UnboundMethod private # Memoize the named method def memoize_method: (Symbol method_name) -> void # Return method builder registry def memoized_methods: () -> Hash[Symbol, MethodBuilder] end # Storage for memoized methods class Memory @memory: Hash[cache_key, untyped] @monitor: ::Monitor # Initialize the memory storage def initialize: (Hash[cache_key, untyped] memory) -> void # Get the value from memory def []: (cache_key name) -> untyped # Store the value in memory def store: (cache_key name, untyped value) -> untyped # Fetch the value from memory, or store it if it does not exist def fetch: (cache_key name, ?untyped default) ?{ () -> untyped } -> untyped # Remove a specific value from memory def delete: (cache_key name) -> untyped # Remove all values from memory def clear: () -> self # A hook that allows Marshal to dump the object def marshal_dump: () -> Hash[cache_key, untyped] # A hook that allows Marshal to load the object def marshal_load: (Hash[cache_key, untyped] hash) -> void private # Resolve the value for a fetch operation def resolve_fetch_value: (cache_key name, bool no_default, untyped default) ?{ () -> untyped } -> untyped end # Build the memoized method class MethodBuilder @descendant: Module @method_name: Symbol @freezer: untyped @original_visibility: Symbol @original_method: UnboundMethod # Raised when the method arity is invalid class InvalidArityError < ArgumentError # Initialize an invalid arity exception def initialize: (Module descendant, Symbol method, Integer arity) -> void end # Raised when a block is passed to a memoized method class BlockNotAllowedError < ArgumentError # Initialize a block not allowed exception def initialize: (Module descendant, Symbol method) -> void end # The original method before memoization attr_reader original_method: UnboundMethod # Initialize an object to build a memoized method def initialize: (Module descendant, Symbol method_name, untyped freezer) -> void # Build a new memoized method def call: () -> self private # Assert the method arity is zero def assert_arity: (Integer arity) -> void # Remove the original method def remove_original_method: () -> void # Create a new memoized method def create_memoized_method: () -> untyped # Set the memoized method visibility to match the original method def set_method_visibility: () -> untyped # Get the visibility of the original method def visibility: () -> Symbol end end dkubb-memoizable-0a01570/spec/000077500000000000000000000000001515165443700161275ustar00rootroot00000000000000dkubb-memoizable-0a01570/spec/integration/000077500000000000000000000000001515165443700204525ustar00rootroot00000000000000dkubb-memoizable-0a01570/spec/integration/child_spec.rb000066400000000000000000000032431515165443700230760ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" class Parent include Memoizable def foo @foo_call_count ||= 0 @foo_call_count += 1 end memoize :foo attr_reader :foo_call_count end class Child < Parent def foo @child_foo_call_count ||= 0 @child_foo_call_count += 1 super + 100 end memoize :foo attr_reader :child_foo_call_count end RSpec.describe Child do subject(:child) { described_class.new } before do child.foo end it "allows subclass to override and memoize a parent memoized method" do expect(child.foo).to eq(101) end it "memoizes the child method" do expect(child.child_foo_call_count).to eq(1) end it "memoizes the parent method" do expect(child.foo_call_count).to eq(1) end it "stores the parent memoized value under its own cache key" do cache = child.instance_variable_get(:@_memoized_method_cache) memory = cache.instance_variable_get(:@memory) expect(memory[[Parent, :foo]]).to eq(1) end it "stores the child memoized value under its own cache key" do cache = child.instance_variable_get(:@_memoized_method_cache) memory = cache.instance_variable_get(:@memory) expect(memory[[described_class, :foo]]).to eq(101) end it "reuses parent memoized value when child cache is cleared" do child.instance_variable_get(:@_memoized_method_cache).delete([described_class, :foo]) child.foo expect(child.foo_call_count).to eq(1) end it "recomputes child value when child cache is cleared" do child.instance_variable_get(:@_memoized_method_cache).delete([described_class, :foo]) child.foo expect(child.child_foo_call_count).to eq(2) end end dkubb-memoizable-0a01570/spec/integration/serializable_spec.rb000066400000000000000000000016051515165443700244610ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" class Serializable include Memoizable def random_number rand(10_000) end memoize :random_number end RSpec.describe Serializable do let(:serializable) do described_class.new end before do # Call the memoized method to trigger lazy memoization serializable.random_number end it "is serializable with Marshal" do expect { Marshal.dump(serializable) }.not_to raise_error end it "is deserializable with Marshal" do serialized = Marshal.dump(serializable) deserialized = Marshal.load(serialized) expect(deserialized).to be_an_instance_of(described_class) end it "preserves memoized values after deserialization" do serialized = Marshal.dump(serializable) deserialized = Marshal.load(serialized) expect(deserialized.random_number).to eql(serializable.random_number) end end dkubb-memoizable-0a01570/spec/shared/000077500000000000000000000000001515165443700173755ustar00rootroot00000000000000dkubb-memoizable-0a01570/spec/shared/call_super_behavior.rb000066400000000000000000000013661515165443700237400ustar00rootroot00000000000000# frozen_string_literal: true RSpec.shared_examples "it calls super" do |method| around do |example| # Restore original method after each example original = "original_#{method}" superclass.class_eval do alias_method original, method example.call undef_method method alias_method method, original end end it "delegates to the superclass ##{method} method" do # This is the most succinct approach I could think of to test whether the # superclass method is called. All of the built-in rspec helpers did not # seem to work for this. called = false superclass.class_eval { define_method(method) { |_| called = true } } expect { subject }.to change { called }.from(false).to(true) end end dkubb-memoizable-0a01570/spec/shared/command_method_behavior.rb000066400000000000000000000002251515165443700245560ustar00rootroot00000000000000# frozen_string_literal: true RSpec.shared_examples_for "a command method" do it "returns self" do expect(subject).to equal(object) end end dkubb-memoizable-0a01570/spec/shared/mocked_events.rb000066400000000000000000000013761515165443700225570ustar00rootroot00000000000000# frozen_string_literal: true RSpec.shared_context "with mocked events" do def register_events(object, method_names) method_names.each do |method_name| allow(object).to receive(method_name) do |*args, &block| events.next.call(object, method_name, *args, &block) end end end def expected_event(object, method_name, *expected_args, &handler) lambda do |*args, &block| expect(args).to eql([object, method_name, *expected_args]) handler.call(&block) end end end RSpec.shared_examples "executes all events" do it "executes all events" do begin subject rescue # subject may raise, should be tested in other examples end expect { events.peek }.to raise_error(StopIteration) end end dkubb-memoizable-0a01570/spec/spec_helper.rb000066400000000000000000000013241515165443700207450ustar00rootroot00000000000000# frozen_string_literal: true begin require "simplecov" SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter] SimpleCov.start do add_filter "/config" add_filter "/spec" add_filter "/vendor" command_name "spec" if RUBY_ENGINE == "ruby" enable_coverage :branch minimum_coverage line: 100, branch: 100 else minimum_coverage line: 100 end end rescue LoadError warn "Warning: simplecov is not installed. Coverage analysis will be skipped." end require "memoizable" require "rspec" # Require spec support files and shared behavior Pathname.glob(Pathname(__dir__).join("{shared,support}", "**", "*.rb")).sort.each do |file| require file.sub_ext("").to_s end dkubb-memoizable-0a01570/spec/unit/000077500000000000000000000000001515165443700171065ustar00rootroot00000000000000dkubb-memoizable-0a01570/spec/unit/memoizable/000077500000000000000000000000001515165443700212325ustar00rootroot00000000000000dkubb-memoizable-0a01570/spec/unit/memoizable/class_methods/000077500000000000000000000000001515165443700240625ustar00rootroot00000000000000dkubb-memoizable-0a01570/spec/unit/memoizable/class_methods/included_spec.rb000066400000000000000000000007561515165443700272200ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Memoizable, ".included" do subject(:include_memoizable) { object.class_eval { include Memoizable } } let(:object) { Class.new } let(:superclass) { Module } it_behaves_like "it calls super", :included it "extends the descendant with module methods" do include_memoizable extended_modules = class << object; included_modules end expect(extended_modules).to include(Memoizable::ModuleMethods) end end dkubb-memoizable-0a01570/spec/unit/memoizable/fixtures/000077500000000000000000000000001515165443700231035ustar00rootroot00000000000000dkubb-memoizable-0a01570/spec/unit/memoizable/fixtures/classes.rb000066400000000000000000000007061515165443700250700ustar00rootroot00000000000000# frozen_string_literal: true module Fixture class Object include Memoizable def required_arguments(foo) end def optional_arguments(foo = nil) end def test "test" end def zero_arity caller end def one_arity(arg) end def public_method caller end protected def protected_method caller end private def private_method caller end end end dkubb-memoizable-0a01570/spec/unit/memoizable/instance_methods/000077500000000000000000000000001515165443700245615ustar00rootroot00000000000000dkubb-memoizable-0a01570/spec/unit/memoizable/instance_methods/freeze_spec.rb000066400000000000000000000012771515165443700274070ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" require File.expand_path("../fixtures/classes", __dir__) RSpec.describe Memoizable::InstanceMethods, "#freeze" do subject(:freeze_object) { object.freeze } let(:described_class) { Class.new(Fixture::Object) } let(:object) { described_class.allocate } before do described_class.memoize(:test) end it_behaves_like "a command method" it "freezes the object" do expect { freeze_object }.to change(object, :frozen?).from(false).to(true) end it "allows methods not yet called to be memoized" do freeze_object first_call = object.test second_call = object.test expect(first_call).to be(second_call) end end dkubb-memoizable-0a01570/spec/unit/memoizable/instance_methods/memoize_spec.rb000066400000000000000000000016611515165443700275710ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" require File.expand_path("../fixtures/classes", __dir__) RSpec.describe Memoizable::InstanceMethods, "#memoize" do subject(:memoize_value) { object.memoize(method => value) } let(:described_class) { Class.new(Fixture::Object) } let(:object) { described_class.new } let(:method) { :test } before do described_class.memoize(method) end context "when the method is not memoized" do let(:value) { "" } it "sets the memoized value for the method to the value" do memoize_value expect(object.send(method)).to be(value) end it_behaves_like "a command method" end context "when the method is already memoized" do let(:value) { double } let(:original) { nil } before do object.memoize(method => original) end it "raises an exception" do expect { memoize_value }.to raise_error(ArgumentError) end end end dkubb-memoizable-0a01570/spec/unit/memoizable/memory/000077500000000000000000000000001515165443700225425ustar00rootroot00000000000000dkubb-memoizable-0a01570/spec/unit/memoizable/memory/clear_spec.rb000066400000000000000000000015621515165443700251730ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Memoizable::Memory, "#clear" do subject(:memory) { described_class.new(foo: 1) } shared_examples "with #clear behaviour" do it "returns self" do expect(memory.clear).to be(memory) end it "removes values" do memory.clear expect { memory[:foo] }.to raise_error(NameError) end end context "without Monitor mocked" do it_behaves_like "with #clear behaviour" end context "with Monitor mocked" do let(:monitor) { instance_double(Monitor) } before do allow(Monitor).to receive_messages(new: monitor) allow(monitor).to receive(:synchronize).and_yield end it_behaves_like "with #clear behaviour" it "synchronizes concurrent updates" do memory.clear expect(monitor).to have_received(:synchronize).once end end end dkubb-memoizable-0a01570/spec/unit/memoizable/memory/delete_spec.rb000066400000000000000000000016161515165443700253470ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Memoizable::Memory, "#delete" do subject(:memory) { described_class.new(foo: 1) } shared_examples "with #delete behaviour" do it "returns value at key" do expect(memory.delete(:foo)).to be(1) end it "removes key" do memory.delete(:foo) expect { memory[:foo] }.to raise_error(NameError) end end context "without Monitor mocked" do it_behaves_like "with #delete behaviour" end context "with Monitor mocked" do let(:monitor) { instance_double(Monitor) } before do allow(Monitor).to receive(:new).and_return(monitor) allow(monitor).to receive(:synchronize).and_yield end it_behaves_like "with #delete behaviour" it "synchronizes concurrent updates" do memory.delete(:foo) expect(monitor).to have_received(:synchronize).once end end end dkubb-memoizable-0a01570/spec/unit/memoizable/memory/element_reference_spec.rb000066400000000000000000000011501515165443700275450ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Memoizable::Memory, "#[]" do subject(:fetch_value) { object[name] } let(:object) { described_class.new({}) } let(:name) { :test } context "when the memory is set" do let(:value) { instance_double("Value") } before do object.store(name, value) end it "returns the expected value" do expect(fetch_value).to be(value) end end context "when the memory is not set" do it "raises an exception" do expect { fetch_value }.to raise_error(NameError, "No method test is memoized") end end end dkubb-memoizable-0a01570/spec/unit/memoizable/memory/fetch_spec.rb000066400000000000000000000114041515165443700251720ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Memoizable::Memory, "#fetch" do subject(:fetch_result) { object.fetch(name) { default } } let(:object) { described_class.new(cache) } let(:cache) { {} } let(:name) { :test } let(:default) { instance_double("Default") } let(:value) { instance_double("Value") } context "when the events are not mocked" do let(:other) { instance_double("Other") } before do # Set other keys in memory object.store(:other, other) object.store(nil, nil) end context "when the memory is set" do before do object.store(name, value) end it "returns the expected value" do expect(fetch_result).to be(value) end it "memoizes the value" do fetch_result expect(object[name]).to be(value) end it "does not overwrite the other key" do fetch_result expect(object[:other]).to be(other) end end context "when the memory is not set" do it "returns the default value" do expect(fetch_result).to be(default) end it "memoizes the default value" do fetch_result expect(object[name]).to be(default) end it "does not overwrite the other key" do fetch_result expect(object[:other]).to be(other) end end context "with a default argument instead of a block" do it "returns the default argument when the key is not found" do expect(object.fetch(name, :default_value)).to be(:default_value) end it "memoizes the default argument" do object.fetch(name, :default_value) expect(object[name]).to be(:default_value) end it "returns the stored value when the key is found" do object.store(name, value) expect(object.fetch(name, :default_value)).to be(value) end it "allows nil as a default value" do expect(object.fetch(name, nil)).to be_nil end end context "with no default argument or block" do it "raises KeyError when the key is not found" do expect { object.fetch(name) }.to raise_error(KeyError, "key not found: :test") end end end context "when the events are mocked" do include_context "with mocked events" let(:cache) do instance_double(Hash).tap do |cache| register_events(cache, %i[fetch []=]) end end let(:monitor) do instance_double(Monitor).tap do |monitor| register_events(monitor, %i[synchronize]) end end before do allow(Monitor).to receive(:new).and_return(monitor) end context "when the memory is set on first #fetch" do let(:events) do Enumerator.new do |events| # First call to cache#fetch returns value events << expected_event(cache, :fetch, name) do value end end end it_behaves_like "executes all events" it "returns the expected value" do expect(fetch_result).to be(value) end it "executes all events" do fetch_result expect { events.peek }.to raise_error(StopIteration) end end context "when the memory is set on second #fetch" do let(:events) do Enumerator.new do |events| # First call to cache#fetch yields events << expected_event(cache, :fetch, name) do |&block| block.call end # Call to monitor#synchronize yields events << expected_event(monitor, :synchronize) do |&block| block.call end # Second call to cache#fetch returns value events << expected_event(cache, :fetch, name) do value end end end it_behaves_like "executes all events" it "returns the expected value" do expect(fetch_result).to be(value) end end context "when the memory is not set on second #fetch" do let(:events) do Enumerator.new do |events| # First call to cache#fetch yields events << expected_event(cache, :fetch, name) do |&block| block.call end # Call to monitor#synchronize yields events << expected_event(monitor, :synchronize) do |&block| block.call end # Second call to cache#fetch yields events << expected_event(cache, :fetch, name) do |&block| block.call end # Call to cache#[]= sets and returns the value events << expected_event(cache, :[]=, name, default) do default end end end it_behaves_like "executes all events" it "returns the default value" do expect(fetch_result).to be(default) end end end end dkubb-memoizable-0a01570/spec/unit/memoizable/memory/marshal_load_spec.rb000066400000000000000000000006441515165443700265330ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Memoizable::Memory, "#marshal_load" do subject(:load_hash) { object.marshal_load(hash) } let(:object) { described_class.allocate } let(:hash) { {test: nil} } it "loads the hash into memory" do load_hash expect(object.fetch(:test)).to be_nil end it "freezes the object" do load_hash expect(object).to be_frozen end end dkubb-memoizable-0a01570/spec/unit/memoizable/memory/store_spec.rb000066400000000000000000000053561515165443700252460ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Memoizable::Memory, "#store" do subject(:store_value) { object.store(name, value) } let(:object) { described_class.new(cache) } let(:cache) { {} } let(:name) { :test } let(:value) { instance_double("Value") } context "when the events are not mocked" do context "when the memory is set" do before do object.store(name, value) end it "raises an exception" do expect do store_value end.to raise_error(ArgumentError, "The method test is already memoized") end end context "when the memory is not set" do it "set the value" do store_value expect(object[name]).to be(value) end it "returns the value" do expect(store_value).to be(value) end end end context "when the events are mocked" do include_context "with mocked events" let(:cache) do instance_double(Hash).tap do |cache| register_events(cache, %i[key? []=]) end end let(:monitor) do instance_double(Monitor).tap do |monitor| register_events(monitor, %i[synchronize]) end end before do allow(Monitor).to receive(:new).and_return(monitor) end context "when the memory is set" do let(:events) do Enumerator.new do |events| # Call to monitor#synchronize yields events << expected_event(monitor, :synchronize) do |&block| block.call end # Call to cache#key? returns true events << expected_event(cache, :key?, name) do true end end end it_behaves_like "executes all events" it "raises an exception" do expect do store_value end.to raise_error(ArgumentError, "The method test is already memoized") end end context "when the memory is not set" do let(:events) do Enumerator.new do |events| # Call to monitor#synchronize yields events << expected_event(monitor, :synchronize) do |&block| block.call end # Call to cache#key? returns false events << expected_event(cache, :key?, name) do false end # Call to cache#[]= sets and returns the value events << expected_event(cache, :[]=, name, value) do allow(cache).to receive(:fetch).with(name).and_return(value) value end end end it_behaves_like "executes all events" it "set the value" do store_value expect(object[name]).to be(value) end it "returns the value" do expect(store_value).to be(value) end end end end dkubb-memoizable-0a01570/spec/unit/memoizable/memory_spec.rb000066400000000000000000000017221515165443700241030ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Memoizable::Memory do let(:object) { described_class.new({}) } it "is frozen" do expect(object).to be_frozen end # This test will raise if mutant removes the @monitor assignment # in the constructor it "depends on the monitor" do expect(object.fetch(:test, :test)).to be(:test) end context "when serialized" do let(:deserialized) { Marshal.load(Marshal.dump(object)) } it "is serializable with Marshal" do expect { Marshal.dump(object) }.not_to raise_error end it "is deserializable with Marshal" do expect(deserialized).to be_an_instance_of(described_class) end it "mantains the same class of cache when deserialized" do original_cache = object.instance_variable_get(:@memory) deserialized_cache = deserialized.instance_variable_get(:@memory) expect(deserialized_cache.class).to eql(original_cache.class) end end end dkubb-memoizable-0a01570/spec/unit/memoizable/method_builder/000077500000000000000000000000001515165443700242205ustar00rootroot00000000000000dkubb-memoizable-0a01570/spec/unit/memoizable/method_builder/call_spec.rb000066400000000000000000000066041515165443700265000ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" require File.expand_path("../fixtures/classes", __dir__) RSpec.describe Memoizable::MethodBuilder, "#call" do subject(:build_method) { object.call } let(:object) { described_class.new(descendant, method_name, freezer) } let(:freezer) { lambda(&:freeze) } let(:instance) { descendant.new } let(:descendant) do Class.new do include Memoizable def public_method __method__.to_s end def protected_method __method__.to_s end protected :protected_method def private_method __method__.to_s end private :private_method def other_method __method__.to_s end memoize :other_method end end shared_examples_for "Memoizable::MethodBuilder#call" do it_behaves_like "a command method" it "creates a method without warning to stderr" do expect { build_method }.not_to output.to_stderr end it "creates a method that is memoized" do build_method first_call = instance.send(method_name) second_call = instance.send(method_name) expect(first_call).to be(second_call) end it "creates a method that returns the expected value" do build_method expect(instance.send(method_name)).to eql(method_name.to_s) end it "creates a method that returns a frozen value" do build_method expect(descendant.new.send(method_name)).to be_frozen end it "creates a method that does not accept a block" do build_method # rubocop:disable Lint/EmptyBlock expect { descendant.new.send(method_name) {} }.to raise_error( # rubocop:enable Lint/EmptyBlock described_class::BlockNotAllowedError, "Cannot pass a block to #{descendant}##{method_name}, it is memoized" ) end it "does not overwrite the cache for other methods", :aggregate_failures do # This test will fail if the cache key is `nil` because the cache # will be populated by the first call and the second call to the # other method will return the wrong cached entry. build_method expect(instance.send(method_name)).to eql(method_name.to_s) expect(instance.other_method).to eql("other_method") end it "uses a composite cache key of [descendant, method_name]" do build_method instance.send(method_name) cache = instance.instance_variable_get(:@_memoized_method_cache) memory = cache.instance_variable_get(:@memory) expect(memory.keys).to include([descendant, method_name]) end end context "with public method" do let(:method_name) { :public_method } it_behaves_like "Memoizable::MethodBuilder#call" it "creates a public memoized method" do build_method expect(descendant).to be_public_method_defined(method_name) end end context "with protected method" do let(:method_name) { :protected_method } it_behaves_like "Memoizable::MethodBuilder#call" it "creates a protected memoized method" do build_method expect(descendant).to be_protected_method_defined(method_name) end end context "with private method" do let(:method_name) { :private_method } it_behaves_like "Memoizable::MethodBuilder#call" it "creates a private memoized method" do build_method expect(descendant).to be_private_method_defined(method_name) end end end dkubb-memoizable-0a01570/spec/unit/memoizable/method_builder/class_methods/000077500000000000000000000000001515165443700270505ustar00rootroot00000000000000dkubb-memoizable-0a01570/spec/unit/memoizable/method_builder/class_methods/new_spec.rb000066400000000000000000000017111515165443700312000ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" require File.expand_path("../../fixtures/classes", __dir__) RSpec.describe Memoizable::MethodBuilder, ".new" do subject(:method_builder) { described_class.new(descendant, method_name, freezer) } let(:descendant) { Fixture::Object } let(:freezer) { lambda(&:freeze) } context "with a zero arity method" do let(:method_name) { :zero_arity } it { is_expected.to be_instance_of(described_class) } it "sets the original method" do # original method is not memoized method = method_builder.original_method.bind(descendant.new) expect(method.call).not_to be(method.call) end end context "with a one arity method" do let(:method_name) { :one_arity } it "raises an exception" do expect { method_builder }.to raise_error( described_class::InvalidArityError, "Cannot memoize Fixture::Object#one_arity, its arity is 1" ) end end end dkubb-memoizable-0a01570/spec/unit/memoizable/method_builder/original_method_spec.rb000066400000000000000000000012711515165443700307240ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Memoizable::MethodBuilder, "#original_method" do subject(:original_method) { object.original_method } let(:object) { described_class.new(descendant, method_name, freezer) } let(:method_name) { :foo } let(:freezer) { lambda(&:freeze) } let(:descendant) do Class.new do def initialize @foo = 0 end def foo @foo += 1 end end end it { is_expected.to be_instance_of(UnboundMethod) } it "returns the original method" do # original method is not memoized method = original_method.bind(descendant.new) expect(method.call).not_to be(method.call) end end dkubb-memoizable-0a01570/spec/unit/memoizable/module_methods/000077500000000000000000000000001515165443700242425ustar00rootroot00000000000000dkubb-memoizable-0a01570/spec/unit/memoizable/module_methods/included_spec.rb000066400000000000000000000011741515165443700273730ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Memoizable::ModuleMethods, "#included" do subject(:include_module) { descendant.instance_exec(object) { |mod| include mod } } let(:object) { Module.new.extend(described_class) } let(:descendant) { Class.new } let(:superclass) { Module } before do # Prevent Module.included from being called through inheritance allow(Memoizable).to receive(:included) end it_behaves_like "it calls super", :included it "includes Memoizable into the descendant" do include_module expect(descendant.included_modules).to include(Memoizable) end end dkubb-memoizable-0a01570/spec/unit/memoizable/module_methods/memoize_spec.rb000066400000000000000000000070031515165443700272460ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" require File.expand_path("../fixtures/classes", __dir__) RSpec.shared_examples_for "memoizes method" do it "memoizes the instance method" do memoize_method instance = object.new first_call = instance.send(method) second_call = instance.send(method) expect(first_call).to be(second_call) end it "creates a zero arity method", unless: RUBY_VERSION == "1.8.7" do memoize_method expect(object.new.method(method).arity).to be_zero end context "when the initializer calls the memoized method" do before do method = self.method object.send(:define_method, :initialize) { send(method) } end it "allows the memoized method to be called within the initializer" do memoize_method expect { object.new }.not_to raise_error end end end RSpec.describe Memoizable::ModuleMethods, "#memoize" do subject(:memoize_method) { object.memoize(method) } let(:object) do stub_const "TestClass", Class.new(Fixture::Object) { def some_state Object.new end } end context "when method has required arguments" do let(:method) { :required_arguments } it "raises error" do expect { memoize_method }.to raise_error( Memoizable::MethodBuilder::InvalidArityError, "Cannot memoize TestClass#required_arguments, its arity is 1" ) end end context "when method has optional arguments" do let(:method) { :optional_arguments } it "raises error" do expect { memoize_method }.to raise_error( Memoizable::MethodBuilder::InvalidArityError, "Cannot memoize TestClass#optional_arguments, its arity is -1" ) end end context "with memoized method that returns generated values" do let(:method) { :some_state } it_behaves_like "a command method" it_behaves_like "memoizes method" it "creates a method that returns a frozen value" do memoize_method expect(object.new.send(method)).to be_frozen end end context "with public method" do let(:method) { :public_method } it_behaves_like "a command method" it_behaves_like "memoizes method" it "is still a public method" do expect(memoize_method).to be_public_method_defined(method) end it "creates a method that returns a frozen value" do memoize_method expect(object.new.send(method)).to be_frozen end end context "with protected method" do let(:method) { :protected_method } it_behaves_like "a command method" it_behaves_like "memoizes method" it "is still a protected method" do expect(memoize_method).to be_protected_method_defined(method) end it "creates a method that returns a frozen value" do memoize_method expect(object.new.send(method)).to be_frozen end end context "with private method" do let(:method) { :private_method } it_behaves_like "a command method" it_behaves_like "memoizes method" it "is still a private method" do expect(memoize_method).to be_private_method_defined(method) end it "creates a method that returns a frozen value" do memoize_method expect(object.new.send(method)).to be_frozen end end context "when the method was already memoized" do let(:method) { :test } before do object.memoize(method) end it "raises an error" do expect { memoize_method }.to raise_error( ArgumentError, "The method test is already memoized" ) end end end dkubb-memoizable-0a01570/spec/unit/memoizable/module_methods/unmemoized_instance_method_spec.rb000066400000000000000000000016031515165443700332010ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" RSpec.describe Memoizable::ModuleMethods, "#unmemoized_instance_method" do subject(:unmemoized_method) { object.unmemoized_instance_method(name) } let(:object) do Class.new do include Memoizable def initialize @foo = 0 end def foo @foo += 1 end memoize :foo end end context "when the method was memoized" do let(:name) { :foo } it { is_expected.to be_instance_of(UnboundMethod) } it "returns the original method" do # original method is not memoized method = unmemoized_method.bind(object.new) expect(method.call).not_to be(method.call) end end context "when the method was not memoized" do let(:name) { :bar } it "raises an exception" do expect { unmemoized_method }.to raise_error(NoMethodError) end end end