├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .ruby-version ├── .yardopts ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── config └── mutant.yml ├── lib ├── memoizable.rb └── memoizable │ ├── instance_methods.rb │ ├── memory.rb │ ├── method_builder.rb │ ├── module_methods.rb │ └── version.rb ├── memoizable.gemspec └── spec ├── integration └── serializable_spec.rb ├── shared ├── call_super_shared_spec.rb ├── command_method_behavior.rb └── mocked_events.rb ├── spec_helper.rb └── unit └── memoizable ├── class_methods └── included_spec.rb ├── fixtures └── classes.rb ├── instance_methods ├── freeze_spec.rb └── memoize_spec.rb ├── memory ├── clear_spec.rb ├── delete_spec.rb ├── element_reference_spec.rb ├── fetch_spec.rb ├── marshal_load_spec.rb └── store_spec.rb ├── memory_spec.rb ├── method_builder ├── call_spec.rb ├── class_methods │ └── new_spec.rb └── original_method_spec.rb └── module_methods ├── included_spec.rb ├── memoize_spec.rb └── unmemoized_instance_method_spec.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | base: 5 | name: Base steps 6 | runs-on: ubuntu-24.04 7 | steps: 8 | - uses: actions/checkout@v3 9 | - name: Check Whitespace 10 | run: git diff --check -- HEAD~1 11 | rspec: 12 | needs: base 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | ruby-version: [3.1, 3.2, head] 18 | os: [ubuntu-latest] 19 | steps: 20 | - uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 2 23 | - uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{ matrix.ruby-version }} 26 | bundler-cache: true # 'ruby/setup-ruby' action provides built-in caching 27 | - name: Check Yard Doc coverage 28 | run: bundle exec yardstick lib 29 | - name: Run tests (ruby-${{ matrix.ruby-version }}) 30 | run: bundle exec rspec 31 | - name: Run mutant 32 | run: | 33 | if bundle show mutant; then 34 | bundle exec mutant run --since HEAD~1 35 | else 36 | echo 'Mutant not supported on ${{ matrix.ruby-version }}' 37 | fi 38 | - name: Upload coverage 39 | uses: actions/upload-artifact@v4 40 | if: success() 41 | with: 42 | name: coverage-${{ matrix.ruby-version }} 43 | path: coverage/ 44 | if-no-files-found: ignore 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | coverage/ 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --backtrace 2 | --color 3 | --format progress 4 | --order random 5 | --profile 6 | --warnings 7 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.2 2 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --quiet 2 | README.md 3 | lib/**/*.rb 4 | LICENSE 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ------------ 3 | 4 | * 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. 5 | * Fork the project. 6 | * Make your feature addition or bug fix. 7 | * Follow this [style guide](https://github.com/dkubb/styleguide). 8 | * 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. 9 | * 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) 10 | * Run "rake ci". This must pass and not show any regressions in the metrics for the code to be merged. 11 | * Send me a pull request. Bonus points for topic branches. 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | group :test do 8 | gem 'rspec', '~> 3.8', '>= 3.8.0' 9 | gem 'yardstick', '~> 0.9', '>= 0.9.9' 10 | 11 | gem 'mutant', '~> 0.12.4' 12 | gem 'mutant-rspec', '~> 0.12.4' 13 | gem 'simplecov', '~> 0.22', '>= 0.22.0' 14 | 15 | source 'https://oss:sxCL1o1navkPi2XnGB5WYBrhpY9iKIPL@gem.mutant.dev' do 16 | gem 'mutant-license' 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Dan Kubb, Erik Michaels-Ober 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Memoizable 2 | 3 | [![Gem Version](http://img.shields.io/gem/v/memoizable.svg)][gem] 4 | ![CI](https://github.com/dkubb/memoizable/workflows/CI/badge.svg) 5 | [![Dependency Status](http://img.shields.io/gemnasium/dkubb/memoizable.svg)][gemnasium] 6 | [![Code Climate](http://img.shields.io/codeclimate/github/dkubb/memoizable.svg)][codeclimate] 7 | 8 | [gem]: https://rubygems.org/gems/memoizable 9 | [gemnasium]: https://gemnasium.com/dkubb/memoizable 10 | [codeclimate]: https://codeclimate.com/github/dkubb/memoizable 11 | 12 | Memoize method return values 13 | 14 | ## Contributing 15 | 16 | See [CONTRIBUTING.md](CONTRIBUTING.md) for details. 17 | 18 | ## Rationale 19 | 20 | Memoization is an optimization that saves the return value of a method so it 21 | doesn't need to be re-computed every time that method is called. For example, 22 | perhaps you've written a method like this: 23 | 24 | ```ruby 25 | class Planet 26 | # This is the equation for the area of a sphere. If it's true for a 27 | # particular instance of a planet, then that planet is spherical. 28 | def spherical? 29 | 4 * Math::PI * radius ** 2 == area 30 | end 31 | end 32 | ``` 33 | 34 | This code will re-compute whether a particular planet is spherical every time 35 | the method is called. If the method is called more than once, it may be more 36 | efficient to save the computed value in an instance variable, like so: 37 | 38 | ```ruby 39 | class Planet 40 | def spherical? 41 | @spherical ||= 4 * Math::PI * radius ** 2 == area 42 | end 43 | end 44 | ``` 45 | 46 | One problem with this approach is that, if the return value is `false`, the 47 | value will still be computed each time the method is called. It also becomes 48 | unweildy for methods that grow to be longer than one line. 49 | 50 | These problems can be solved by mixing-in the `Memoizable` module and memoizing 51 | the method. 52 | 53 | ```ruby 54 | require 'memoizable' 55 | 56 | class Planet 57 | include Memoizable 58 | def spherical? 59 | 4 * Math::PI * radius ** 2 == area 60 | end 61 | memoize :spherical? 62 | end 63 | ``` 64 | 65 | ## Warning 66 | 67 | The example above assumes that the radius and area of a planet will not change 68 | over time. This seems like a reasonable assumption but such an assumption is 69 | not safe in every domain. If it was possible for one of the attributes to 70 | change between method calls, memoizing that value could produce the wrong 71 | result. Please keep this in mind when considering which methods to memoize. 72 | 73 | Supported Ruby Versions 74 | ----------------------- 75 | 76 | This library aims to support and is tested against the following Ruby 77 | implementations: 78 | 79 | * Ruby 2.1 80 | * Ruby 2.2 81 | * Ruby 2.3 82 | * Ruby 2.4 83 | * Ruby 2.5 84 | * Ruby 2.6 85 | * Ruby 2.7 86 | * [JRuby][jruby] 87 | 88 | [jruby]: http://jruby.org/ 89 | 90 | If something doesn't work on one of these versions, it's a bug. 91 | 92 | This library may inadvertently work (or seem to work) on other Ruby versions or 93 | implementations, however support will only be provided for the implementations 94 | listed above. 95 | 96 | If you would like this library to support another Ruby version or 97 | implementation, you may volunteer to be a maintainer. Being a maintainer 98 | entails making sure all tests run and pass on that implementation. When 99 | something breaks on your implementation, you will be responsible for providing 100 | patches in a timely fashion. If critical issues for a particular implementation 101 | exist at the time of a major release, support for that Ruby version may be 102 | dropped. 103 | 104 | ## Copyright 105 | 106 | Copyright © 2013 Dan Kubb, Erik Michaels-Ober. See LICENSE for details. 107 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'bundler' 4 | require 'rspec/core/rake_task' 5 | 6 | Bundler::GemHelper.install_tasks 7 | RSpec::Core::RakeTask.new(:spec) 8 | 9 | task :test => :spec 10 | task :default => :spec 11 | -------------------------------------------------------------------------------- /config/mutant.yml: -------------------------------------------------------------------------------- 1 | --- 2 | usage: opensource 3 | integration: 4 | name: rspec 5 | requires: 6 | - memoizable 7 | matcher: 8 | subjects: 9 | - Memoizable* 10 | mutation: 11 | operators: full 12 | timeout: 1.0 13 | -------------------------------------------------------------------------------- /lib/memoizable.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'monitor' 4 | 5 | require 'memoizable/instance_methods' 6 | require 'memoizable/method_builder' 7 | require 'memoizable/module_methods' 8 | require 'memoizable/memory' 9 | require 'memoizable/version' 10 | 11 | # Allow methods to be memoized 12 | module Memoizable 13 | include InstanceMethods 14 | 15 | # Default freezer 16 | Freezer = lambda { |object| object.freeze }.freeze 17 | 18 | # Hook called when module is included 19 | # 20 | # @param [Module] descendant 21 | # the module or class including Memoizable 22 | # 23 | # @return [self] 24 | # 25 | # @api private 26 | def self.included(descendant) 27 | super 28 | descendant.extend(ModuleMethods) 29 | end 30 | private_class_method :included 31 | 32 | end # Memoizable 33 | -------------------------------------------------------------------------------- /lib/memoizable/instance_methods.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Memoizable 4 | 5 | # Methods mixed in to memoizable instances 6 | module InstanceMethods 7 | 8 | # Freeze the object 9 | # 10 | # @example 11 | # object.freeze # object is now frozen 12 | # 13 | # @return [Object] 14 | # 15 | # @api public 16 | def freeze 17 | memoized_method_cache # initialize method cache 18 | super() 19 | end 20 | 21 | # Sets a memoized value for a method 22 | # 23 | # @example 24 | # object.memoize(hash: 12345) 25 | # 26 | # @param [Hash{Symbol => Object}] data 27 | # the data to memoize 28 | # 29 | # @return [self] 30 | # 31 | # @api public 32 | def memoize(data) 33 | data.each { |name, value| memoized_method_cache.store(name, value) } 34 | self 35 | end 36 | 37 | private 38 | 39 | # The memoized method results 40 | # 41 | # @return [Hash] 42 | # 43 | # @api private 44 | def memoized_method_cache 45 | @_memoized_method_cache ||= Memory.new({}) 46 | end 47 | 48 | end # InstanceMethods 49 | end # Memoizable 50 | -------------------------------------------------------------------------------- /lib/memoizable/memory.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Memoizable 4 | 5 | # Storage for memoized methods 6 | class Memory 7 | 8 | # Initialize the memory storage for memoized methods 9 | # 10 | # @param [Hash] memory 11 | # 12 | # @return [undefined] 13 | # 14 | # @api private 15 | def initialize(memory) 16 | @memory = memory 17 | @monitor = Monitor.new 18 | freeze 19 | end 20 | 21 | # Get the value from memory 22 | # 23 | # @example 24 | # 25 | # memory = Memoizable::Memory.new(foo: 1) 26 | # memory[:foo] # => 1 27 | # 28 | # @param [Symbol] name 29 | # 30 | # @return [Object] 31 | # 32 | # @api public 33 | def [](name) 34 | fetch(name) do 35 | fail NameError, "No method #{name} is memoized" 36 | end 37 | end 38 | 39 | # Store the value in memory 40 | # 41 | # @example 42 | # memory = Memoizable::Memory.new(foo: 1) 43 | # memory.store(:foo, 2) 44 | # memory[:foo] # => 2 45 | # 46 | # @param [Symbol] name 47 | # @param [Object] value 48 | # 49 | # @return [undefined] 50 | # 51 | # @api public 52 | def store(name, value) 53 | @monitor.synchronize do 54 | if @memory.key?(name) 55 | fail ArgumentError, "The method #{name} is already memoized" 56 | else 57 | @memory[name] = value 58 | end 59 | end 60 | end 61 | 62 | # Fetch the value from memory, or store it if it does not exist 63 | # 64 | # @example 65 | # memory = Memoizable::Memory.new(foo: 1) 66 | # memory.fetch(:foo) { 2 } # => 1 67 | # memory.fetch(:bar) { 2 } # => 2 68 | # memory[:bar] # => 2 69 | # 70 | # @param [Symbol] name 71 | # 72 | # @yieldreturn [Object] 73 | # the value to memoize 74 | # 75 | # @return [Object] 76 | # 77 | # @api public 78 | def fetch(name) 79 | @memory.fetch(name) do # check for the key 80 | @monitor.synchronize do # acquire a lock if the key is not found 81 | @memory.fetch(name) do # recheck under lock 82 | @memory[name] = yield # set the value 83 | end 84 | end 85 | end 86 | end 87 | 88 | # Remove a specific value from memory 89 | # 90 | # @example 91 | # memory = Memoizable::Memory.new(foo: 1) 92 | # memory.delete(:foo) 93 | # 94 | # @param [Symbol] name 95 | # 96 | # @return [Object] 97 | # 98 | # @api public 99 | def delete(name) 100 | @monitor.synchronize do 101 | @memory.delete(name) 102 | end 103 | end 104 | 105 | # Remove all values from memory 106 | # 107 | # @example 108 | # memory = Memoizable::Memory.new(foo: 1) 109 | # memory.clear # => memory 110 | # 111 | # @return [self] 112 | # 113 | # @api public 114 | def clear 115 | @monitor.synchronize do 116 | @memory.clear 117 | end 118 | self 119 | end 120 | 121 | # A hook that allows Marshal to dump the object 122 | # 123 | # @example 124 | # memory = Memoizable::Memory.new(foo: 1) 125 | # Marshal.dump(memory) # => "\x04\bU:\x17Memoizable::Memory{\x06:\bfooi\x06" 126 | # 127 | # @return [Hash] 128 | # A hash used to populate the internal memory 129 | # 130 | # @api public 131 | def marshal_dump 132 | @memory 133 | end 134 | 135 | # A hook that allows Marshal to load the object 136 | # 137 | # @example 138 | # memory = Memoizable::Memory.new(foo: 1) 139 | # Marshal.load(Marshal.dump(memory)) # => #1}> 140 | # 141 | # @param [Hash] hash 142 | # A hash used to populate the internal memory 143 | # 144 | # @return [undefined] 145 | # 146 | # @api public 147 | def marshal_load(hash) 148 | initialize(hash) 149 | end 150 | 151 | end # Memory 152 | end # Memoizable 153 | -------------------------------------------------------------------------------- /lib/memoizable/method_builder.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Memoizable 4 | 5 | # Build the memoized method 6 | class MethodBuilder 7 | 8 | # Raised when the method arity is invalid 9 | class InvalidArityError < ArgumentError 10 | 11 | # Initialize an invalid arity exception 12 | # 13 | # @param [Module] descendant 14 | # @param [Symbol] method 15 | # @param [Integer] arity 16 | # 17 | # @api private 18 | def initialize(descendant, method, arity) 19 | super("Cannot memoize #{descendant}##{method}, its arity is #{arity}") 20 | end 21 | 22 | end # InvalidArityError 23 | 24 | # Raised when a block is passed to a memoized method 25 | class BlockNotAllowedError < ArgumentError 26 | 27 | # Initialize a block not allowed exception 28 | # 29 | # @param [Module] descendant 30 | # @param [Symbol] method 31 | # 32 | # @api private 33 | def initialize(descendant, method) 34 | super("Cannot pass a block to #{descendant}##{method}, it is memoized") 35 | end 36 | 37 | end # BlockNotAllowedError 38 | 39 | # The original method before memoization 40 | # 41 | # @example 42 | # method_builder.original_method # => :foo 43 | # 44 | # @return [UnboundMethod] 45 | # 46 | # @api public 47 | attr_reader :original_method 48 | 49 | # Initialize an object to build a memoized method 50 | # 51 | # @param [Module] descendant 52 | # @param [Symbol] method_name 53 | # @param [#call] freezer 54 | # 55 | # @return [undefined] 56 | # 57 | # @api private 58 | def initialize(descendant, method_name, freezer) 59 | @descendant = descendant 60 | @method_name = method_name 61 | @freezer = freezer 62 | @original_visibility = visibility 63 | @original_method = descendant.instance_method(@method_name) 64 | assert_arity(original_method.arity) 65 | end 66 | 67 | # Build a new memoized method 68 | # 69 | # @example 70 | # method_builder.call # => creates new method 71 | # 72 | # @return [MethodBuilder] 73 | # 74 | # @api public 75 | def call 76 | remove_original_method 77 | create_memoized_method 78 | set_method_visibility 79 | self 80 | end 81 | 82 | private 83 | 84 | # Assert the method arity is zero 85 | # 86 | # @param [Integer] arity 87 | # 88 | # @return [undefined] 89 | # 90 | # @raise [InvalidArityError] 91 | # 92 | # @api private 93 | def assert_arity(arity) 94 | if arity.nonzero? 95 | fail InvalidArityError.new(@descendant, @method_name, arity) 96 | end 97 | end 98 | 99 | # Remove the original method 100 | # 101 | # @return [undefined] 102 | # 103 | # @api private 104 | def remove_original_method 105 | name = @method_name 106 | @descendant.module_eval { undef_method(name) } 107 | end 108 | 109 | # Create a new memoized method 110 | # 111 | # @return [undefined] 112 | # 113 | # @api private 114 | def create_memoized_method 115 | name, method, freezer = @method_name, @original_method, @freezer 116 | @descendant.module_eval do 117 | define_method(name) do |&block| 118 | fail BlockNotAllowedError.new(self.class, name) if block 119 | memoized_method_cache.fetch(name) do 120 | freezer.call(method.bind(self).call) 121 | end 122 | end 123 | end 124 | end 125 | 126 | # Set the memoized method visibility to match the original method 127 | # 128 | # @return [undefined] 129 | # 130 | # @api private 131 | def set_method_visibility 132 | @descendant.__send__(@original_visibility, @method_name) 133 | end 134 | 135 | # Get the visibility of the original method 136 | # 137 | # @return [Symbol] 138 | # 139 | # @api private 140 | def visibility 141 | if @descendant.private_method_defined?(@method_name) then :private 142 | elsif @descendant.protected_method_defined?(@method_name) then :protected 143 | else :public 144 | end 145 | end 146 | 147 | end # MethodBuilder 148 | end # Memoizable 149 | -------------------------------------------------------------------------------- /lib/memoizable/module_methods.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Memoizable 4 | 5 | # Methods mixed in to memoizable singleton classes 6 | module ModuleMethods 7 | include Memoizable 8 | 9 | # Return default deep freezer 10 | # 11 | # @return [#call] 12 | # 13 | # @api private 14 | def freezer 15 | Freezer 16 | end 17 | 18 | # Memoize a list of methods 19 | # 20 | # @example 21 | # memoize :hash 22 | # 23 | # @param [Array] methods 24 | # a list of methods to memoize 25 | # 26 | # @return [self] 27 | # 28 | # @api public 29 | def memoize(*methods) 30 | methods.each(&method(:memoize_method)) 31 | self 32 | end 33 | 34 | # Return unmemoized instance method 35 | # 36 | # @example 37 | # 38 | # class Foo 39 | # include Memoizable 40 | # 41 | # def bar 42 | # end 43 | # memoize :bar 44 | # end 45 | # 46 | # Foo.unmemoized_instance_method(:bar) 47 | # 48 | # @param [Symbol] name 49 | # 50 | # @return [UnboundMethod] 51 | # the memoized method 52 | # 53 | # @raise [NameError] 54 | # raised if the method is unknown 55 | # 56 | # @api public 57 | def unmemoized_instance_method(name) 58 | memoized_methods[name].original_method 59 | end 60 | 61 | private 62 | 63 | # Memoize the named method 64 | # 65 | # @param [Symbol] method_name 66 | # a method name to memoize 67 | # 68 | # @return [undefined] 69 | # 70 | # @api private 71 | def memoize_method(method_name) 72 | fail ArgumentError, "The method #{method_name} is already memoized" if memoized_methods.key?(method_name) 73 | memoized_methods[method_name] = MethodBuilder.new( 74 | self, 75 | method_name, 76 | freezer 77 | ).call 78 | end 79 | 80 | # Return method builder registry 81 | # 82 | # @return [Hash] 83 | # 84 | # @api private 85 | def memoized_methods 86 | @_memoized_methods ||= {} 87 | end 88 | 89 | end # ModuleMethods 90 | end # Memoizable 91 | -------------------------------------------------------------------------------- /lib/memoizable/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Memoizable 4 | 5 | # Gem version 6 | VERSION = '0.4.2'.freeze 7 | 8 | end # Memoizable 9 | -------------------------------------------------------------------------------- /memoizable.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require File.expand_path('../lib/memoizable/version', __FILE__) 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = 'memoizable' 7 | gem.version = Memoizable::VERSION.dup 8 | gem.authors = ['Dan Kubb', 'Erik Michaels-Ober'] 9 | gem.email = ['dan.kubb@gmail.com', 'sferik@gmail.com'] 10 | gem.description = 'Memoize method return values' 11 | gem.summary = gem.description 12 | gem.homepage = 'https://github.com/dkubb/memoizable' 13 | gem.license = 'MIT' 14 | 15 | gem.require_paths = %w[lib] 16 | gem.files = %w[CONTRIBUTING.md LICENSE.md README.md memoizable.gemspec] + Dir['lib/**/*.rb'] 17 | gem.extra_rdoc_files = Dir['**/*.md'] 18 | 19 | gem.required_ruby_version = '>= 3.1' 20 | end 21 | -------------------------------------------------------------------------------- /spec/integration/serializable_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | class Serializable 6 | include Memoizable 7 | 8 | def random_number 9 | rand(10000) 10 | end 11 | memoize :random_number 12 | end 13 | 14 | describe 'A serializable object' do 15 | let(:serializable) do 16 | Serializable.new 17 | end 18 | 19 | before do 20 | serializable.random_number # Call the memoized method to trigger lazy memoization 21 | end 22 | 23 | it 'is serializable with Marshal' do 24 | expect { Marshal.dump(serializable) }.not_to raise_error 25 | end 26 | 27 | it 'is deserializable with Marshal' do 28 | serialized = Marshal.dump(serializable) 29 | deserialized = Marshal.load(serialized) 30 | 31 | expect(deserialized).to be_an_instance_of(Serializable) 32 | expect(deserialized.random_number).to eql(serializable.random_number) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/shared/call_super_shared_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | shared_examples 'it calls super' do |method| 4 | around do |example| 5 | # Restore original method after each example 6 | original = "original_#{method}" 7 | superclass.class_eval do 8 | alias_method original, method 9 | example.call 10 | undef_method method 11 | alias_method method, original 12 | end 13 | end 14 | 15 | it "delegates to the superclass ##{method} method" do 16 | # This is the most succinct approach I could think of to test whether the 17 | # superclass method is called. All of the built-in rspec helpers did not 18 | # seem to work for this. 19 | called = false 20 | superclass.class_eval { define_method(method) { |_| called = true } } 21 | expect { subject }.to change { called }.from(false).to(true) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/shared/command_method_behavior.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | shared_examples_for 'a command method' do 4 | it 'returns self' do 5 | should equal(object) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/shared/mocked_events.rb: -------------------------------------------------------------------------------- 1 | shared_context 'mocked events' do 2 | def register_events(object, method_names) 3 | method_names.each do |method_name| 4 | allow(object).to receive(method_name) do |*args, &block| 5 | events.next.call(object, method_name, *args, &block) 6 | end 7 | end 8 | end 9 | 10 | def expected_event(object, method_name, *expected_args, &handler) 11 | ->(*args, &block) do 12 | expect(args).to eql([object, method_name, *expected_args]) 13 | handler.call(&block) 14 | end 15 | end 16 | end 17 | 18 | shared_examples 'executes all events' do 19 | it 'executes all events' do 20 | begin 21 | subject 22 | rescue 23 | # subject may raise, should be tested in other examples 24 | end 25 | expect { events.peek }.to raise_error(StopIteration) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | begin 4 | require 'simplecov' 5 | 6 | SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter] 7 | 8 | SimpleCov.start do 9 | add_filter '/config' 10 | add_filter '/spec' 11 | add_filter '/vendor' 12 | command_name 'spec' 13 | minimum_coverage 100 14 | end 15 | rescue LoadError 16 | $stderr.puts 'Warning: simplecov is not installed. Coverage analysis will be skipped.' 17 | end 18 | 19 | require 'memoizable' 20 | require 'rspec' 21 | 22 | # Require spec support files and shared behavior 23 | Pathname.glob(Pathname(__dir__).join('{shared,support}', '**', '*.rb')).sort.each do |file| 24 | require file.sub_ext('').to_s 25 | end 26 | 27 | RSpec.configure do |config| 28 | config.raise_errors_for_deprecations! 29 | 30 | config.expect_with :rspec do |expect_with| 31 | expect_with.syntax = :expect 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/unit/memoizable/class_methods/included_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe Memoizable, '.included' do 6 | subject { object.class_eval { include Memoizable } } 7 | 8 | let(:object) { Class.new } 9 | let(:superclass) { Module } 10 | 11 | it_behaves_like 'it calls super', :included 12 | 13 | it 'extends the descendant with module methods' do 14 | subject 15 | extended_modules = class << object; included_modules end 16 | expect(extended_modules).to include(Memoizable::ModuleMethods) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/unit/memoizable/fixtures/classes.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Fixture 4 | class Object 5 | include Memoizable 6 | 7 | def required_arguments(foo) 8 | end 9 | 10 | def optional_arguments(foo = nil) 11 | end 12 | 13 | def test 14 | 'test' 15 | end 16 | 17 | def zero_arity 18 | caller 19 | end 20 | 21 | def one_arity(arg) 22 | end 23 | 24 | def public_method 25 | caller 26 | end 27 | 28 | protected 29 | 30 | def protected_method 31 | caller 32 | end 33 | 34 | private 35 | 36 | def private_method 37 | caller 38 | end 39 | 40 | end # class Object 41 | end # module Fixture 42 | -------------------------------------------------------------------------------- /spec/unit/memoizable/instance_methods/freeze_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | require File.expand_path('../../fixtures/classes', __FILE__) 5 | 6 | describe Memoizable::InstanceMethods, '#freeze' do 7 | subject { object.freeze } 8 | 9 | let(:described_class) { Class.new(Fixture::Object) } 10 | 11 | before do 12 | described_class.memoize(:test) 13 | end 14 | 15 | let(:object) { described_class.allocate } 16 | 17 | it_should_behave_like 'a command method' 18 | 19 | it 'freezes the object' do 20 | expect { subject }.to change(object, :frozen?).from(false).to(true) 21 | end 22 | 23 | it 'allows methods not yet called to be memoized' do 24 | subject 25 | expect(object.test).to be(object.test) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/unit/memoizable/instance_methods/memoize_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | require File.expand_path('../../fixtures/classes', __FILE__) 5 | 6 | describe Memoizable::InstanceMethods, '#memoize' do 7 | subject { object.memoize(method => value) } 8 | 9 | let(:described_class) { Class.new(Fixture::Object) } 10 | let(:object) { described_class.new } 11 | let(:method) { :test } 12 | 13 | before do 14 | described_class.memoize(method) 15 | end 16 | 17 | context 'when the method is not memoized' do 18 | let(:value) { String.new } 19 | 20 | it 'sets the memoized value for the method to the value' do 21 | subject 22 | expect(object.send(method)).to be(value) 23 | end 24 | 25 | it_should_behave_like 'a command method' 26 | end 27 | 28 | context 'when the method is already memoized' do 29 | let(:value) { double } 30 | let(:original) { nil } 31 | 32 | before do 33 | object.memoize(method => original) 34 | end 35 | 36 | it 'raises an exception' do 37 | expect { subject }.to raise_error(ArgumentError) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/unit/memoizable/memory/clear_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Memoizable::Memory, '#clear' do 4 | subject { described_class.new(foo: 1) } 5 | 6 | shared_context '#clear behaviour' do 7 | it 'returns self' do 8 | expect(subject.clear).to be(subject) 9 | end 10 | 11 | it 'removes values' do 12 | subject.clear 13 | 14 | expect { subject[:foo] }.to raise_error(NameError) 15 | end 16 | end 17 | 18 | context 'without Monitor mocked' do 19 | include_examples '#clear behaviour' 20 | end 21 | 22 | context 'with Monitor mocked' do 23 | let(:monitor) { instance_double(Monitor) } 24 | 25 | before do 26 | allow(Monitor).to receive_messages(new: monitor) 27 | allow(monitor).to receive(:synchronize).and_yield 28 | end 29 | 30 | include_examples '#clear behaviour' 31 | 32 | it 'synchronizes concurrent updates' do 33 | subject.clear 34 | 35 | expect(monitor).to have_received(:synchronize).once 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/unit/memoizable/memory/delete_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Memoizable::Memory, '#delete' do 4 | subject { described_class.new(foo: 1) } 5 | 6 | shared_context '#delete behaviour' do 7 | it 'returns value at key' do 8 | expect(subject.delete(:foo)).to be(1) 9 | end 10 | 11 | it 'removes key' do 12 | expect(subject[:foo]).to be(1) 13 | 14 | subject.delete(:foo) 15 | 16 | expect { subject[:foo] }.to raise_error(NameError) 17 | end 18 | end 19 | 20 | context 'without Monitor mocked' do 21 | include_examples '#delete behaviour' 22 | end 23 | 24 | context 'with Monitor mocked' do 25 | let(:monitor) { instance_double(Monitor) } 26 | 27 | before do 28 | allow(Monitor).to receive(:new).and_return(monitor) 29 | allow(monitor).to receive(:synchronize).and_yield 30 | end 31 | 32 | include_examples '#delete behaviour' 33 | 34 | it 'synchronizes concurrent updates' do 35 | subject.delete(:foo) 36 | 37 | expect(monitor).to have_received(:synchronize).once 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/unit/memoizable/memory/element_reference_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Memoizable::Memory, '#[]' do 4 | subject { object[name] } 5 | 6 | let(:object) { described_class.new({}) } 7 | let(:name) { :test } 8 | 9 | context 'when the memory is set' do 10 | let(:value) { instance_double('Value') } 11 | 12 | before do 13 | object.store(name, value) 14 | end 15 | 16 | it 'returns the expected value' do 17 | expect(subject).to be(value) 18 | end 19 | end 20 | 21 | context 'when the memory is not set' do 22 | it 'raises an exception' do 23 | expect { subject }.to raise_error(NameError, 'No method test is memoized') 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/unit/memoizable/memory/fetch_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Memoizable::Memory, '#fetch' do 4 | subject { object.fetch(name) { default } } 5 | 6 | let(:object) { described_class.new(cache) } 7 | let(:cache) { {} } 8 | let(:name) { :test } 9 | let(:default) { instance_double('Default') } 10 | let(:value) { instance_double('Value') } 11 | 12 | context 'when the events are not mocked' do 13 | let(:other) { instance_double('Other') } 14 | 15 | before do 16 | # Set other keys in memory 17 | object.store(:other, other) 18 | object.store(nil, nil) 19 | end 20 | 21 | context 'when the memory is set' do 22 | before do 23 | object.store(name, value) 24 | end 25 | 26 | it 'returns the expected value' do 27 | expect(subject).to be(value) 28 | end 29 | 30 | it 'memoizes the value' do 31 | subject 32 | expect(object[name]).to be(value) 33 | end 34 | 35 | it 'does not overwrite the other key' do 36 | subject 37 | expect(object[:other]).to be(other) 38 | end 39 | end 40 | 41 | context 'when the memory is not set' do 42 | it 'returns the default value' do 43 | expect(subject).to be(default) 44 | end 45 | 46 | it 'memoizes the default value' do 47 | subject 48 | expect(object[name]).to be(default) 49 | end 50 | 51 | it 'does not overwrite the other key' do 52 | subject 53 | expect(object[:other]).to be(other) 54 | end 55 | end 56 | end 57 | 58 | context 'when the events are mocked' do 59 | include_context 'mocked events' 60 | 61 | let(:cache) do 62 | instance_double(Hash).tap do |cache| 63 | register_events(cache, %i[fetch []=]) 64 | end 65 | end 66 | 67 | let(:monitor) do 68 | instance_double(Monitor).tap do |monitor| 69 | register_events(monitor, %i[synchronize]) 70 | end 71 | end 72 | 73 | before do 74 | allow(Monitor).to receive(:new).and_return(monitor) 75 | end 76 | 77 | context 'when the memory is set on first #fetch' do 78 | include_examples 'executes all events' 79 | 80 | let(:events) do 81 | Enumerator.new do |events| 82 | # First call to cache#fetch returns value 83 | events << expected_event(cache, :fetch, name) do 84 | value 85 | end 86 | end 87 | end 88 | 89 | it 'returns the expected value' do 90 | expect(subject).to be(value) 91 | end 92 | 93 | it 'executes all events' do 94 | subject 95 | expect { events.peek }.to raise_error(StopIteration) 96 | end 97 | end 98 | 99 | context 'when the memory is set on second #fetch' do 100 | include_examples 'executes all events' 101 | 102 | let(:events) do 103 | Enumerator.new do |events| 104 | # First call to cache#fetch yields 105 | events << expected_event(cache, :fetch, name) do |&block| 106 | block.call 107 | end 108 | 109 | # Call to monitor#synchronize yields 110 | events << expected_event(monitor, :synchronize) do |&block| 111 | block.call 112 | end 113 | 114 | # Second call to cache#fetch returns value 115 | events << expected_event(cache, :fetch, name) do 116 | value 117 | end 118 | end 119 | end 120 | 121 | it 'returns the expected value' do 122 | expect(subject).to be(value) 123 | end 124 | end 125 | 126 | context 'when the memory is not set on second #fetch' do 127 | include_examples 'executes all events' 128 | 129 | let(:events) do 130 | Enumerator.new do |events| 131 | # First call to cache#fetch yields 132 | events << expected_event(cache, :fetch, name) do |&block| 133 | block.call 134 | end 135 | 136 | # Call to monitor#synchronize yields 137 | events << expected_event(monitor, :synchronize) do |&block| 138 | block.call 139 | end 140 | 141 | # Second call to cache#fetch yields 142 | events << expected_event(cache, :fetch, name) do |&block| 143 | block.call 144 | end 145 | 146 | # Call to cache#[]= sets and returns the value 147 | events << expected_event(cache, :[]=, name, default) do 148 | default 149 | end 150 | end 151 | end 152 | 153 | it 'returns the default value' do 154 | expect(subject).to be(default) 155 | end 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /spec/unit/memoizable/memory/marshal_load_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Memoizable::Memory, '#marshal_load' do 4 | subject { object.marshal_load(hash) } 5 | 6 | let(:object) { described_class.allocate } 7 | let(:hash) { { test: nil } } 8 | 9 | it 'loads the hash into memory' do 10 | subject 11 | expect(object.fetch(:test)).to be(nil) 12 | end 13 | 14 | it 'freezes the object' do 15 | subject 16 | expect(object).to be_frozen 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/unit/memoizable/memory/store_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Memoizable::Memory, '#store' do 4 | subject { object.store(name, value) } 5 | 6 | let(:object) { described_class.new(cache) } 7 | let(:cache) { {} } 8 | let(:name) { :test } 9 | let(:value) { instance_double('Value') } 10 | 11 | context 'when the events are not mocked' do 12 | context 'when the memory is set' do 13 | before do 14 | object.store(name, value) 15 | end 16 | 17 | it 'raises an exception' do 18 | expect { subject }.to raise_error(ArgumentError, 'The method test is already memoized') 19 | end 20 | end 21 | 22 | context 'when the memory is not set' do 23 | it 'set the value' do 24 | subject 25 | expect(object[name]).to be(value) 26 | end 27 | 28 | it 'returns the value' do 29 | expect(subject).to be(value) 30 | end 31 | end 32 | end 33 | 34 | context 'when the events are mocked' do 35 | include_context 'mocked events' 36 | 37 | let(:cache) do 38 | instance_double(Hash).tap do |cache| 39 | register_events(cache, %i[key? []=]) 40 | end 41 | end 42 | 43 | let(:monitor) do 44 | instance_double(Monitor).tap do |monitor| 45 | register_events(monitor, %i[synchronize]) 46 | end 47 | end 48 | 49 | before do 50 | allow(Monitor).to receive(:new).and_return(monitor) 51 | end 52 | 53 | context 'when the memory is set' do 54 | include_examples 'executes all events' 55 | 56 | let(:events) do 57 | Enumerator.new do |events| 58 | # Call to monitor#synchronize yields 59 | events << expected_event(monitor, :synchronize) do |&block| 60 | block.call 61 | end 62 | 63 | # Call to cache#key? returns true 64 | events << expected_event(cache, :key?, name) do 65 | true 66 | end 67 | end 68 | end 69 | 70 | it 'raises an exception' do 71 | expect { subject }.to raise_error(ArgumentError, 'The method test is already memoized') 72 | end 73 | end 74 | 75 | context 'when the memory is not set' do 76 | include_examples 'executes all events' 77 | 78 | let(:events) do 79 | Enumerator.new do |events| 80 | # Call to monitor#synchronize yields 81 | events << expected_event(monitor, :synchronize) do |&block| 82 | block.call 83 | end 84 | 85 | # Call to cache#key? returns false 86 | events << expected_event(cache, :key?, name) do 87 | false 88 | end 89 | 90 | # Call to cache#[]= sets and returns the value 91 | events << expected_event(cache, :[]=, name, value) do 92 | allow(cache).to receive(:fetch).with(name).and_return(value) 93 | value 94 | end 95 | end 96 | end 97 | 98 | it 'set the value' do 99 | subject 100 | expect(object[name]).to be(value) 101 | end 102 | 103 | it 'returns the value' do 104 | expect(subject).to be(value) 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/unit/memoizable/memory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Memoizable::Memory do 4 | let(:object) { Memoizable::Memory.new({}) } 5 | 6 | it 'is frozen' do 7 | expect(object).to be_frozen 8 | end 9 | 10 | # This test will raise if mutant removes the @monitor assignment 11 | # in the constructor 12 | it 'depends on the monitor' do 13 | expect(object.fetch(:test) { :test }).to be(:test) 14 | end 15 | 16 | context "serialization" do 17 | let(:deserialized) { Marshal.load(Marshal.dump(object)) } 18 | 19 | it 'is serializable with Marshal' do 20 | expect { Marshal.dump(object) }.not_to raise_error 21 | end 22 | 23 | it 'is deserializable with Marshal' do 24 | expect(deserialized).to be_an_instance_of(Memoizable::Memory) 25 | end 26 | 27 | it 'mantains the same class of cache when deserialized' do 28 | original_cache = object.instance_variable_get(:@memory) 29 | deserialized_cache = deserialized.instance_variable_get(:@memory) 30 | 31 | expect(deserialized_cache.class).to eql(original_cache.class) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/unit/memoizable/method_builder/call_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | require File.expand_path('../../fixtures/classes', __FILE__) 5 | 6 | describe Memoizable::MethodBuilder, '#call' do 7 | subject { object.call } 8 | 9 | let(:object) { described_class.new(descendant, method_name, freezer) } 10 | let(:freezer) { lambda { |object| object.freeze } } 11 | let(:instance) { descendant.new } 12 | 13 | let(:descendant) do 14 | Class.new do 15 | include Memoizable 16 | 17 | def public_method 18 | __method__.to_s 19 | end 20 | 21 | def protected_method 22 | __method__.to_s 23 | end 24 | protected :protected_method 25 | 26 | def private_method 27 | __method__.to_s 28 | end 29 | private :private_method 30 | 31 | def other_method 32 | __method__.to_s 33 | end 34 | memoize :other_method 35 | end 36 | end 37 | 38 | shared_examples_for 'Memoizable::MethodBuilder#call' do 39 | it_should_behave_like 'a command method' 40 | 41 | it 'creates a method without warning to stderr' do 42 | expect { subject }.to_not output.to_stderr 43 | end 44 | 45 | it 'creates a method that is memoized' do 46 | subject 47 | expect(instance.send(method_name)).to be(instance.send(method_name)) 48 | end 49 | 50 | it 'creates a method that returns the expected value' do 51 | subject 52 | expect(instance.send(method_name)).to eql(method_name.to_s) 53 | end 54 | 55 | it 'creates a method that returns a frozen value' do 56 | subject 57 | expect(descendant.new.send(method_name)).to be_frozen 58 | end 59 | 60 | it 'creates a method that does not accept a block' do 61 | subject 62 | expect { descendant.new.send(method_name) {} }.to raise_error( 63 | described_class::BlockNotAllowedError, 64 | "Cannot pass a block to #{descendant}##{method_name}, it is memoized" 65 | ) 66 | end 67 | 68 | it 'does not overwrite the cache for other methods' do 69 | # This test will fail if the cache key is `nil` because the cache 70 | # will be populated by the first call and the second call to the 71 | # other method will return the wrong cached entry. 72 | subject 73 | expect(instance.send(method_name)).to eql(method_name.to_s) 74 | expect(instance.other_method).to eql('other_method') 75 | end 76 | end 77 | 78 | context 'public method' do 79 | let(:method_name) { :public_method } 80 | 81 | it_should_behave_like 'Memoizable::MethodBuilder#call' 82 | 83 | it 'creates a public memoized method' do 84 | subject 85 | expect(descendant).to be_public_method_defined(method_name) 86 | end 87 | end 88 | 89 | context 'protected method' do 90 | let(:method_name) { :protected_method } 91 | 92 | it_should_behave_like 'Memoizable::MethodBuilder#call' 93 | 94 | it 'creates a protected memoized method' do 95 | subject 96 | expect(descendant).to be_protected_method_defined(method_name) 97 | end 98 | 99 | end 100 | 101 | context 'private method' do 102 | let(:method_name) { :private_method } 103 | 104 | it_should_behave_like 'Memoizable::MethodBuilder#call' 105 | 106 | it 'creates a private memoized method' do 107 | subject 108 | expect(descendant).to be_private_method_defined(method_name) 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/unit/memoizable/method_builder/class_methods/new_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | require File.expand_path('../../../fixtures/classes', __FILE__) 5 | 6 | describe Memoizable::MethodBuilder, '.new' do 7 | subject { described_class.new(descendant, method_name, freezer) } 8 | 9 | let(:descendant) { Fixture::Object } 10 | let(:freezer) { lambda { |object| object.freeze } } 11 | 12 | context 'with a zero arity method' do 13 | let(:method_name) { :zero_arity } 14 | 15 | it { should be_instance_of(described_class) } 16 | 17 | it 'sets the original method' do 18 | # original method is not memoized 19 | method = subject.original_method.bind(descendant.new) 20 | expect(method.call).to_not be(method.call) 21 | end 22 | end 23 | 24 | context 'with a one arity method' do 25 | let(:method_name) { :one_arity } 26 | 27 | it 'raises an exception' do 28 | expect { subject }.to raise_error( 29 | described_class::InvalidArityError, 30 | 'Cannot memoize Fixture::Object#one_arity, its arity is 1' 31 | ) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/unit/memoizable/method_builder/original_method_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe Memoizable::MethodBuilder, '#original_method' do 6 | subject { object.original_method } 7 | 8 | let(:object) { described_class.new(descendant, method_name, freezer) } 9 | let(:method_name) { :foo } 10 | let(:freezer) { lambda { |object| object.freeze } } 11 | 12 | let(:descendant) do 13 | Class.new do 14 | def initialize 15 | @foo = 0 16 | end 17 | 18 | def foo 19 | @foo += 1 20 | end 21 | end 22 | end 23 | 24 | it { should be_instance_of(UnboundMethod) } 25 | 26 | it 'returns the original method' do 27 | # original method is not memoized 28 | method = subject.bind(descendant.new) 29 | expect(method.call).to_not be(method.call) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/unit/memoizable/module_methods/included_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe Memoizable::ModuleMethods, '#included' do 6 | subject { descendant.instance_exec(object) { |mod| include mod } } 7 | 8 | let(:object) { Module.new.extend(described_class) } 9 | let(:descendant) { Class.new } 10 | let(:superclass) { Module } 11 | 12 | before do 13 | # Prevent Module.included from being called through inheritance 14 | allow(Memoizable).to receive(:included) 15 | end 16 | 17 | it_behaves_like 'it calls super', :included 18 | 19 | it 'includes Memoizable into the descendant' do 20 | subject 21 | expect(descendant.included_modules).to include(Memoizable) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/unit/memoizable/module_methods/memoize_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | require File.expand_path('../../fixtures/classes', __FILE__) 5 | 6 | shared_examples_for 'memoizes method' do 7 | it 'memoizes the instance method' do 8 | subject 9 | instance = object.new 10 | expect(instance.send(method)).to be(instance.send(method)) 11 | end 12 | 13 | it 'creates a zero arity method', :unless => RUBY_VERSION == '1.8.7' do 14 | subject 15 | expect(object.new.method(method).arity).to be_zero 16 | end 17 | 18 | context 'when the initializer calls the memoized method' do 19 | before do 20 | method = self.method 21 | object.send(:define_method, :initialize) { send(method) } 22 | end 23 | 24 | it 'allows the memoized method to be called within the initializer' do 25 | subject 26 | expect { object.new }.to_not raise_error 27 | end 28 | end 29 | end 30 | 31 | describe Memoizable::ModuleMethods, '#memoize' do 32 | subject { object.memoize(method) } 33 | 34 | let(:object) do 35 | stub_const 'TestClass', Class.new(Fixture::Object) { 36 | def some_state 37 | Object.new 38 | end 39 | } 40 | end 41 | 42 | context 'on method with required arguments' do 43 | let(:method) { :required_arguments } 44 | 45 | it 'should raise error' do 46 | expect { subject }.to raise_error( 47 | Memoizable::MethodBuilder::InvalidArityError, 48 | 'Cannot memoize TestClass#required_arguments, its arity is 1' 49 | ) 50 | end 51 | end 52 | 53 | context 'on method with optional arguments' do 54 | let(:method) { :optional_arguments } 55 | 56 | it 'should raise error' do 57 | expect { subject }.to raise_error( 58 | Memoizable::MethodBuilder::InvalidArityError, 59 | 'Cannot memoize TestClass#optional_arguments, its arity is -1' 60 | ) 61 | end 62 | end 63 | 64 | context 'memoized method that returns generated values' do 65 | let(:method) { :some_state } 66 | 67 | it_should_behave_like 'a command method' 68 | it_should_behave_like 'memoizes method' 69 | 70 | it 'creates a method that returns a frozen value' do 71 | subject 72 | expect(object.new.send(method)).to be_frozen 73 | end 74 | end 75 | 76 | context 'public method' do 77 | let(:method) { :public_method } 78 | 79 | it_should_behave_like 'a command method' 80 | it_should_behave_like 'memoizes method' 81 | 82 | it 'is still a public method' do 83 | should be_public_method_defined(method) 84 | end 85 | 86 | it 'creates a method that returns a frozen value' do 87 | subject 88 | expect(object.new.send(method)).to be_frozen 89 | end 90 | end 91 | 92 | context 'protected method' do 93 | let(:method) { :protected_method } 94 | 95 | it_should_behave_like 'a command method' 96 | it_should_behave_like 'memoizes method' 97 | 98 | it 'is still a protected method' do 99 | should be_protected_method_defined(method) 100 | end 101 | 102 | it 'creates a method that returns a frozen value' do 103 | subject 104 | expect(object.new.send(method)).to be_frozen 105 | end 106 | end 107 | 108 | context 'private method' do 109 | let(:method) { :private_method } 110 | 111 | it_should_behave_like 'a command method' 112 | it_should_behave_like 'memoizes method' 113 | 114 | it 'is still a private method' do 115 | should be_private_method_defined(method) 116 | end 117 | 118 | it 'creates a method that returns a frozen value' do 119 | subject 120 | expect(object.new.send(method)).to be_frozen 121 | end 122 | end 123 | 124 | context 'when the method was already memoized' do 125 | let(:method) { :test } 126 | 127 | before do 128 | object.memoize(method) 129 | end 130 | 131 | it 'raises an error' do 132 | expect { subject }.to raise_error( 133 | ArgumentError, 134 | 'The method test is already memoized' 135 | ) 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /spec/unit/memoizable/module_methods/unmemoized_instance_method_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe Memoizable::ModuleMethods, '#unmemoized_instance_method' do 6 | subject { object.unmemoized_instance_method(name) } 7 | 8 | let(:object) do 9 | Class.new do 10 | include Memoizable 11 | 12 | def initialize 13 | @foo = 0 14 | end 15 | 16 | def foo 17 | @foo += 1 18 | end 19 | 20 | memoize :foo 21 | end 22 | end 23 | 24 | context 'when the method was memoized' do 25 | let(:name) { :foo } 26 | 27 | it { should be_instance_of(UnboundMethod) } 28 | 29 | it 'returns the original method' do 30 | # original method is not memoized 31 | method = subject.bind(object.new) 32 | expect(method.call).to_not be(method.call) 33 | end 34 | end 35 | 36 | context 'when the method was not memoized' do 37 | let(:name) { :bar } 38 | 39 | it 'raises an exception' do 40 | expect { subject }.to raise_error(NoMethodError) 41 | end 42 | end 43 | end 44 | --------------------------------------------------------------------------------