[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    # We want to run on external PRs, but not on our own internal PRs as they'll be run on push event\n    if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'tycooon/memery'\n\n    strategy:\n      fail-fast: false\n      matrix:\n        ruby: [\"3.2\", \"3.3\", \"3.4\"]\n\n    name: ${{ matrix.ruby }}\n\n    steps:\n    - uses: actions/checkout@v4\n\n    - uses: ruby/setup-ruby@v1\n      with:\n        ruby-version: ${{ matrix.ruby }}\n        bundler-cache: true\n\n    - run: bundle exec rake\n\n    - uses: coverallsapp/github-action@v2\n      with:\n        github-token: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "/.bundle/\n/.yardoc\n/coverage/\n/doc/\n/pkg/\n/spec/reports/\n/tmp/\n.ruby-version\n\n# rspec failure tracking\n.rspec_status\n"
  },
  {
    "path": ".rspec",
    "content": "--format documentation\n--color\n--require spec_helper\n--warnings\n"
  },
  {
    "path": ".rubocop.yml",
    "content": "inherit_gem:\n  rubocop-config-umbrellio: lib/rubocop.yml\n\nAllCops:\n  DisplayCopNames: true\n  TargetRubyVersion: 3.2\n\nNaming/MethodParameterName:\n  AllowedNames: [\"x\", \"y\", \"z\"]\n\nRSpec/EmptyLineAfterHook:\n  Enabled: false\n"
  },
  {
    "path": "Gemfile",
    "content": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\ngemspec\n\ngem \"activesupport\"\ngem \"benchmark-ips\"\ngem \"benchmark-memory\"\ngem \"bundler\"\ngem \"pry\"\ngem \"rake\"\ngem \"rspec\"\ngem \"rubocop-config-umbrellio\"\ngem \"simplecov\"\ngem \"simplecov-lcov\"\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2017 Yuri Smirnov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Memery   [![Gem Version](https://badge.fury.io/rb/memery.svg)](https://badge.fury.io/rb/memery) ![Build Status](https://github.com/tycooon/memery/actions/workflows/ci.yml/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/tycooon/memery/badge.svg?branch=master)](https://coveralls.io/github/tycooon/memery?branch=master)\n\nMemery is a Ruby gem that simplifies memoization of method return values. In Ruby, memoization typically looks like this:\n\n```ruby\ndef user\n  @user ||= User.find(some_id)\nend\n```\n\nHowever, this approach fails if the calculated result can be `nil` or `false`, or if the method uses arguments. Additionally, multi-line methods require extra `begin`/`end` blocks:\n\n```ruby\ndef user\n  @user ||= begin\n    some_id = calculate_id\n    klass = calculate_klass\n    klass.find(some_id)\n  end\nend\n```\n\nTo handle these situations, memoization gems like Memery exist. The example above can be rewritten using Memery as follows:\n\n```ruby\nmemoize def user\n  some_id = calculate_id\n  klass = calculate_klass\n  klass.find(some_id)\nend\n```\n\n## Installation\n\nAdd `gem \"memery\"` to your Gemfile.\n\n## Usage\n\n```ruby\nclass A\n  include Memery\n\n  memoize def call\n    puts \"calculating\"\n    42\n  end\n\n  # Alternatively:\n  # def call\n  #   ...\n  # end\n  # memoize :call\nend\n\na = A.new\na.call # => 42\na.call # => 42\na.call # => 42\n# \"calculating\" will only be printed once.\n\na.call { 1 } # => 42\n# \"calculating\" will be printed again because passing a block disables memoization.\n```\n\nMemoization works with methods that take arguments. The memoization is based on these arguments using an internal hash, so the following will work as expected:\n\n```ruby\nclass A\n  include Memery\n\n  memoize def call(arg1, arg2)\n    puts \"calculating\"\n    arg1 + arg2\n  end\nend\n\na = A.new\na.call(1, 5) # => 6\na.call(2, 15) # => 17\na.call(1, 5) # => 6\n# \"calculating\" will be printed twice, once for each unique argument list.\n```\n\nFor class methods:\n\n```ruby\nclass B\n  class << self\n    include Memery\n\n    memoize def call\n      puts \"calculating\"\n      42\n    end\n  end\nend\n\nB.call # => 42\nB.call # => 42\nB.call # => 42\n# \"calculating\" will only be printed once.\n```\n\n### Conditional Memoization\n\n```ruby\nclass A\n  include Memery\n\n  attr_accessor :environment\n\n  def call\n    puts \"calculating\"\n    42\n  end\n\n  memoize :call, condition: -> { environment == 'production' }\nend\n\na = A.new\na.environment = 'development'\na.call # => 42\n# calculating\na.call # => 42\n# calculating\na.call # => 42\n# calculating\n# Text will be printed every time because result of condition block is `false`.\n\na.environment = 'production'\na.call # => 42\n# calculating\na.call # => 42\na.call # => 42\n# Text will be printed only once because there is memoization\n# with `true` result of condition block.\n```\n\n### Memoization with Time-to-Live (TTL)\n\n```ruby\nclass A\n  include Memery\n\n  def call\n    puts \"calculating\"\n    42\n  end\n\n  memoize :call, ttl: 3 # seconds\nend\n\na = A.new\na.call # => 42\n# calculating\na.call # => 42\na.call # => 42\n# Text will be printed again only after 3 seconds of time-to-live.\n# 3 seconds later...\na.call # => 42\n# calculating\na.call # => 42\na.call # => 42\n# another 3 seconds later...\na.call # => 42\n# calculating\na.call # => 42\na.call # => 42\n```\n\n### Checking if a Method is Memoized\n\n```ruby\nclass A\n  include Memery\n\n  memoize def call\n    puts \"calculating\"\n    42\n  end\n\n  def execute\n    puts \"non-memoized\"\n  end\nend\n\na = A.new\n\na.memoized?(:call) # => true\na.memoized?(:execute) # => false\n```\n\n### Marshal-compatible Memoization\n\nIn order for objects to be marshaled and loaded in a different Ruby process,\nhashed arguments must be disabled in order for memoized values to be retained.\nNote that this can have a performance impact if the memoized method contains\narguments.\n\n```ruby\nMemery.use_hashed_arguments = false\n\nclass A\n  include Memery\n\n  memoize def call\n    puts \"calculating\"\n    42\n  end\nend\n\na = A.new\na.call\n\nMarshal.dump(a)\n# => \"\\x04\\bo:\\x06A\\x06:\\x1D@_memery_memoized_values{\\x06:\\tcallS:3Memery::ClassMethods::MemoizationModule::Cache\\a:\\vresulti/:\\ttimef\\x14663237.14822323\"\n\n# ...in another Ruby process:\na = Marshal.load(\"\\x04\\bo:\\x06A\\x06:\\x1D@_memery_memoized_values{\\x06:\\tcallS:3Memery::ClassMethods::MemoizationModule::Cache\\a:\\vresulti/:\\ttimef\\x14663237.14822323\")\na.call # => 42\n```\n\n## Differences from Other Gems\n\nMemery is similar to [Memoist](https://github.com/matthewrudy/memoist), but it doesn't override methods. Instead, it uses Ruby 2's `Module.prepend` feature. This approach is cleaner, allowing you to inspect the original method body with `method(:x).super_method.source`, and it ensures that subclasses' methods function properly. If you redefine a memoized method in a subclass, it won't be memoized by default. You can memoize it normally without needing an awkward `identifier: ` argument, and it will just work:\n\n```ruby\nclass A\n  include Memery\n\n  memoize def x(param)\n    param\n  end\nend\n\nclass B < A\n  memoize def x(param)\n    super(2) * param\n  end\nend\n\nb = B.new\nb.x(1) # => 2\nb.x(2) # => 4\nb.x(3) # => 6\n\nb.instance_variable_get(:@_memery_memoized_values)\n# => {:x_70318201388120=>{[1]=>2, [2]=>4, [3]=>6}, :x_70318184636620=>{[2]=>2}}\n```\n\nNote how both methods' return values are cached separately without interfering with each other.\n\nAnother key difference is that Memery doesn't change the method's signature (no extra `reload` parameter). If you need an unmemoized result, simply create an unmemoized version of the method:\n\n```ruby\nmemoize def users\n  get_users\nend\n\ndef get_users\n  # ...\nend\n```\n\nAlternatively, you can clear the entire instance's cache:\n\n```ruby\na.clear_memery_cache!\n```\n\nYou can also provide a block, though this approach is somewhat hacky:\n\n```ruby\na.users {}\n```\n\n## Object Shape Optimization\n\nIn Ruby 3.2, a new optimization called \"object shape\" was introduced, which can have negative interactions with dynamically added instance variables. Memery minimizes this impact by introducing only one new instance variable after initialization (`@_memery_memoized_values`). If you need to ensure a specific object shape, you can call `clear_memery_cache!` in your initializer to set the instance variable ahead of time.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/tycooon/memery.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n\n## Author\n\nCreated by Yuri Smirnov.\n"
  },
  {
    "path": "Rakefile",
    "content": "# frozen_string_literal: true\n\nrequire \"bundler/gem_tasks\"\nrequire \"rspec/core/rake_task\"\nrequire \"rubocop/rake_task\"\n\nRSpec::Core::RakeTask.new(:spec)\nRuboCop::RakeTask.new(:lint)\n\ntask default: %i[lint spec]\n\ndesc \"run benchmark\"\ntask :benchmark do\n  require_relative \"benchmark\"\nend\n"
  },
  {
    "path": "benchmark.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"bundler/setup\"\nBundler.setup\n\nrequire \"benchmark\"\nrequire \"benchmark/ips\"\nrequire \"benchmark/memory\"\n\nputs \"```ruby\"\nputs File.read(__FILE__)\nputs \"```\"\nputs\nputs \"### Output\"\nputs\nputs \"```\"\n\nrequire_relative \"lib/memery\"\n\nclass Foo\n  class << self\n    include Memery\n\n    def base_find(char)\n      (\"a\"..\"k\").find { |letter| letter == char }\n    end\n\n    memoize def find_z\n      base_find(\"z\")\n    end\n\n    memoize def find_new(char)\n      base_find(char)\n    end\n\n    memoize def find_optional(*)\n      base_find(\"z\")\n    end\n  end\nend\n\ndef test_no_args\n  Foo.find_z\nend\n\ndef test_with_args\n  Foo.find_new(\"d\")\nend\n\ndef test_empty_args\n  Foo.find_optional\nend\n\nBenchmark.ips do |x|\n  x.report(\"test_no_args\") { test_no_args }\nend\n\nBenchmark.memory do |x|\n  x.report(\"test_no_args\") { 100.times { test_no_args } }\nend\n\nBenchmark.ips do |x|\n  x.report(\"test_empty_args\") { test_empty_args }\nend\n\nBenchmark.memory do |x|\n  x.report(\"test_empty_args\") { 100.times { test_empty_args } }\nend\n\nBenchmark.ips do |x|\n  x.report(\"test_with_args\") { test_with_args }\nend\n\nBenchmark.memory do |x|\n  x.report(\"test_with_args\") { 100.times { test_with_args } }\nend\n\nMemery.use_hashed_arguments = false\nBenchmark.ips do |x|\n  x.report(\"test_with_args_no_hash\") { test_with_args }\nend\n\nBenchmark.memory do |x|\n  x.report(\"test_with_args_no_hash\") { 100.times { test_with_args } }\nend\nMemery.use_hashed_arguments = true\n\nputs \"```\"\n"
  },
  {
    "path": "lib/memery/version.rb",
    "content": "# frozen_string_literal: true\n\nmodule Memery\n  VERSION = \"1.8.0\"\nend\n"
  },
  {
    "path": "lib/memery.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"memery/version\"\n\nmodule Memery\n  class << self\n    attr_accessor :use_hashed_arguments\n\n    def monotonic_clock\n      Process.clock_gettime(Process::CLOCK_MONOTONIC)\n    end\n  end\n\n  @use_hashed_arguments = true\n\n  OUR_BLOCK = lambda do\n    extend(ClassMethods)\n    include(InstanceMethods)\n    extend ModuleMethods if instance_of?(Module)\n  end\n\n  private_constant :OUR_BLOCK\n\n  module ModuleMethods\n    def included(base = nil, &block)\n      if base.nil? && block\n        super do\n          instance_exec(&block)\n          instance_exec(&OUR_BLOCK)\n        end\n      else\n        base.instance_exec(&OUR_BLOCK)\n      end\n    end\n  end\n\n  extend ModuleMethods\n\n  module ClassMethods\n    def memoize(*method_names, condition: nil, ttl: nil)\n      if method_names.empty?\n        @_memery_memoize_next_method = { condition: condition, ttl: ttl }\n        return\n      end\n      prepend_memery_module!\n      method_names.each do |method_name|\n        define_memoized_method!(method_name, condition: condition, ttl: ttl)\n      end\n      method_names.length > 1 ? method_names : method_names.first\n    end\n\n    def memoized?(method_name)\n      return false unless defined?(@_memery_module)\n\n      @_memery_module.method_defined?(method_name) ||\n      @_memery_module.private_method_defined?(method_name)\n    end\n\n    def method_added(name)\n      super\n\n      return unless @_memery_memoize_next_method\n\n      memoize(name, **@_memery_memoize_next_method)\n      @_memery_memoize_next_method = nil\n    end\n\n    private\n\n    def prepend_memery_module!\n      return if defined?(@_memery_module)\n      @_memery_module = Module.new { extend MemoizationModule }\n      prepend(@_memery_module)\n    end\n\n    def define_memoized_method!(method_name, **)\n      @_memery_module.define_memoized_method!(self, method_name, **)\n    end\n\n    module MemoizationModule\n      Cache = Struct.new(:result, :time) do\n        def fresh?(ttl)\n          return true if ttl.nil?\n          Memery.monotonic_clock <= time + ttl\n        end\n      end\n\n      # rubocop:disable Metrics/MethodLength\n      def define_memoized_method!(klass, method_name, condition: nil, ttl: nil)\n        # Include a suffix in the method key to differentiate between methods of the same name\n        # being memoized throughout a class inheritance hierarchy\n        method_key = \"#{method_name}_#{klass.name || object_id}\"\n        original_visibility = method_visibility(klass, method_name)\n\n        define_method(method_name) do |*args, &block|\n          if block || (condition && !instance_exec(&condition))\n            return super(*args, &block)\n          end\n\n          cache_store = (@_memery_memoized_values ||= {})\n          cache_key = if args.empty?\n                        method_key\n                      else\n                        key_parts = [method_key, *args]\n                        Memery.use_hashed_arguments ? key_parts.hash : key_parts\n                      end\n          cache = cache_store[cache_key]\n\n          return cache.result if cache&.fresh?(ttl)\n\n          result = super(*args)\n          new_cache = Cache.new(result, Memery.monotonic_clock)\n          cache_store[cache_key] = new_cache\n\n          result\n        end\n\n        ruby2_keywords(method_name)\n        send(original_visibility, method_name)\n      end\n      # rubocop:enable Metrics/MethodLength\n\n      private\n\n      def method_visibility(klass, method_name)\n        if klass.private_method_defined?(method_name)\n          :private\n        elsif klass.protected_method_defined?(method_name)\n          :protected\n        elsif klass.public_method_defined?(method_name)\n          :public\n        else\n          raise ArgumentError, \"Method #{method_name} is not defined on #{klass}\"\n        end\n      end\n    end\n\n    private_constant :MemoizationModule\n  end\n\n  module InstanceMethods\n    def clear_memery_cache!\n      @_memery_memoized_values = {}\n    end\n  end\nend\n"
  },
  {
    "path": "memery.gemspec",
    "content": "# frozen_string_literal: true\n\nlib = File.expand_path(\"lib\", __dir__)\n$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)\nrequire \"memery/version\"\n\nGem::Specification.new do |spec|\n  spec.required_ruby_version = \">= 3.2.0\"\n\n  spec.name          = \"memery\"\n  spec.version       = Memery::VERSION\n  spec.authors       = [\"Yuri Smirnov\"]\n  spec.email         = [\"tycoooon@gmail.com\"]\n\n  spec.summary       = \"A gem for memoization.\"\n  spec.description   = \"Memery is a gem for memoization.\"\n  spec.homepage      = \"https://github.com/tycooon/memery\"\n  spec.license       = \"MIT\"\n\n  spec.files         = `git ls-files -z`.split(\"\\x0\").reject do |f|\n    f.match(%r{^(test|spec|features)/})\n  end\n  spec.require_paths = [\"lib\"]\nend\n"
  },
  {
    "path": "spec/memery_spec.rb",
    "content": "# frozen_string_literal: true\n\n# rubocop:disable Style/MutableConstant\nCALLS = []\nB_CALLS = []\n# rubocop:enable Style/MutableConstant\n\nclass A\n  include Memery\n\n  attr_accessor :environment\n\n  memoize def m\n    m_private\n  end\n\n  memoize\n  def m_different_line\n    CALLS << :m_different_line\n    :m_different_line\n  end\n\n  def not_memoized; end\n\n  memoize def m_nil\n    m_protected\n  end\n\n  memoize def m_args(x, y)\n    CALLS << [x, y]\n    [x, y]\n  end\n\n  memoize def m_kwargs(x, y: 42)\n    CALLS << [x, y]\n    [x, y]\n  end\n\n  memoize def m_double_splat(x, **kwargs)\n    CALLS << [x, kwargs]\n    [x, kwargs]\n  end\n\n  def m_condition\n    CALLS << __method__\n    __method__\n  end\n\n  memoize :m_condition, condition: -> { environment == \"production\" }\n\n  def m_ttl(x, y)\n    CALLS << [x, y]\n    [x, y]\n  end\n\n  memoize :m_ttl, ttl: 3\n\n  protected\n\n  memoize def m_protected\n    CALLS << nil\n    nil\n  end\n\n  private\n\n  memoize def m_private\n    CALLS << :m\n    :m\n  end\nend\n\nclass B < A\n  memoize def m_args(x, y)\n    B_CALLS << [x, y]\n    super(1, 2)\n    100\n  end\nend\n\nmodule M\n  include Memery\n\n  memoize def m\n    CALLS << :m\n    :m\n  end\n\n  memoize\n\n  def m_different_line\n    CALLS << :m_different_line\n    :m_different_line\n  end\n\n  def not_memoized; end\n\n  private\n\n  memoize def m_private; end\nend\n\nclass C\n  include M\n\n  memoize def m_class\n    CALLS << __method__\n    __method__\n  end\nend\n\nclass D\n  class << self\n    include Memery\n\n    memoize def m_args(x, y)\n      CALLS << [x, y]\n      [x, y]\n    end\n  end\nend\n\nclass E\n  extend Forwardable\n  def_delegator :a, :m\n\n  include Memery\n\n  memoize def a\n    A.new\n  end\nend\n\nclass F\n  include Memery\n\n  def m; end\nend\n\nclass G\n  include Memery\n\n  def self.macro(name)\n    define_method(:macro_received) { name }\n  end\n\n  macro memoize def g; end\nend\n\nclass H\n  include Memery\n\n  [:a, :b, :m, :n, :x, :y].each do |name|\n    define_method(name) do\n      CALLS << name\n      name\n    end\n  end\n\n  memoize :m, :n\n  memoize :x, :y, ttl: 3\nend\n\nRSpec.describe Memery do\n  subject(:a) { A.new }\n\n  before { CALLS.clear }\n  before { B_CALLS.clear }\n  before { Memery.use_hashed_arguments = true }\n\n  let(:unmemoized_class) do\n    Class.new do\n      include Memery\n      attr_reader :a, :b, :m, :n, :x, :y\n    end\n  end\n\n  context \"methods without args\" do\n    specify do\n      values = [ a.m, a.m_nil, a.m, a.m_nil ]\n      expect(values).to eq([:m, nil, :m, nil])\n      expect(CALLS).to eq([:m, nil])\n    end\n  end\n\n  context \"methods without args memoize on new line\" do\n    specify do\n      values = [ a.m_different_line, a.m_nil, a.m_different_line, a.m_nil ]\n      expect(values).to eq([:m_different_line, nil, :m_different_line, nil])\n      expect(CALLS).to eq([:m_different_line, nil])\n    end\n  end\n\n  context \"flushing cache\" do\n    specify do\n      values = [ a.m, a.m ]\n      a.clear_memery_cache!\n      values << a.m\n      expect(values).to eq([:m, :m, :m])\n      expect(CALLS).to eq([:m, :m])\n    end\n  end\n\n  context \"method with args\" do\n    specify do\n      values = [ a.m_args(1, 1), a.m_args(1, 1), a.m_args(1, 2) ]\n      expect(values).to eq([[1, 1], [1, 1], [1, 2]])\n      expect(CALLS).to eq([[1, 1], [1, 2]])\n    end\n\n    context \"receiving Hash-like object\" do\n      let(:object_class) do\n        Struct.new(:first_name, :last_name) do\n          # For example, Sequel models have such implicit coercion,\n          # which conflicts with `**kwargs`.\n          alias_method :to_hash, :to_h\n        end\n      end\n\n      let(:object) { object_class.new(\"John\", \"Wick\") }\n\n      specify do\n        values = [ a.m_args(1, object), a.m_args(1, object), a.m_args(1, 2) ]\n        expect(values).to eq([[1, object], [1, object], [1, 2]])\n        expect(CALLS).to eq([[1, object], [1, 2]])\n      end\n    end\n  end\n\n  context \"method with keyword args\" do\n    specify do\n      values = [ a.m_kwargs(1, y: 2), a.m_kwargs(1, y: 2), a.m_kwargs(1, y: 3) ]\n      expect(values).to eq([[1, 2], [1, 2], [1, 3]])\n      expect(CALLS).to eq([[1, 2], [1, 3]])\n    end\n  end\n\n  context \"method with double splat argument\" do\n    specify do\n      values = [ a.m_double_splat(1, y: 2), a.m_double_splat(1, y: 2), a.m_double_splat(1, y: 3) ]\n      expect(values).to eq([[1, { y: 2 }], [1, { y: 2 }], [1, { y: 3 }]])\n      expect(CALLS).to eq([[1, { y: 2 }], [1, { y: 3 }]])\n    end\n  end\n\n  context \"calling method with block\" do\n    specify do\n      values = []\n      values << a.m_args(1, 1) { nil }\n      values << a.m_args(1, 1) { nil }\n\n      expect(values).to eq([[1, 1], [1, 1]])\n      expect(CALLS).to eq([[1, 1], [1, 1]])\n    end\n  end\n\n  context \"calling private method\" do\n    specify do\n      expect { a.m_private }.to raise_error(NoMethodError, /private method/)\n    end\n  end\n\n  context \"calling protected method\" do\n    specify do\n      expect { a.m_protected }.to raise_error(NoMethodError, /protected method/)\n    end\n  end\n\n  context \"Chaining macros\" do\n    subject(:g) { G.new }\n\n    specify do\n      expect(g.macro_received).to eq :g\n    end\n  end\n\n  context \"inherited class\" do\n    subject(:b) { B.new }\n\n    specify do\n      values = [ b.m_args(1, 1), b.m_args(1, 2), b.m_args(1, 1) ]\n      expect(values).to eq([100, 100, 100])\n      expect(CALLS).to eq([[1, 2]])\n      expect(B_CALLS).to eq([[1, 1], [1, 2]])\n    end\n  end\n\n  context \"anonymous inherited class\" do\n    let(:anonymous_class) do\n      Class.new(A) do\n        memoize def m_args(x, y)\n          B_CALLS << [x, y]\n          super(1, 2)\n          100\n        end\n      end\n    end\n\n    subject(:b) { anonymous_class.new }\n\n    specify do\n      values = [ b.m_args(1, 1), b.m_args(1, 2), b.m_args(1, 1) ]\n      expect(values).to eq([100, 100, 100])\n      expect(CALLS).to eq([[1, 2]])\n      expect(B_CALLS).to eq([[1, 1], [1, 2]])\n    end\n  end\n\n  context \"module\" do\n    subject(:c) { C.new }\n\n    specify do\n      values = [c.m, c.m, c.m]\n      expect(values).to eq([:m, :m, :m])\n      expect(CALLS).to eq([:m])\n    end\n\n    specify do\n      values = [c.m_different_line, c.m_different_line, c.m_different_line]\n      expect(values).to eq([:m_different_line, :m_different_line, :m_different_line])\n      expect(CALLS).to eq([:m_different_line])\n    end\n\n    context \"memoization in class\" do\n      specify do\n        values = [c.m_class, c.m_class, c.m_class]\n        expect(values).to eq([:m_class, :m_class, :m_class])\n        expect(CALLS).to eq([:m_class])\n      end\n    end\n  end\n\n  context \"module with self.included method defined\" do\n    subject(:c) { C.new }\n\n    before { C.include(some_mixin) }\n\n    let(:some_mixin) do\n      Module.new do\n        extend ActiveSupport::Concern\n        include Memery\n\n        included do\n          attr_accessor :a\n        end\n      end\n    end\n\n    it \"doesn't override existing method\" do\n      c.a = 15\n      expect(c.a).to eq(15)\n    end\n  end\n\n  context \"class method with args\" do\n    subject(:d) { D }\n\n    specify do\n      values = [ d.m_args(1, 1), d.m_args(1, 1), d.m_args(1, 2) ]\n      expect(values).to eq([[1, 1], [1, 1], [1, 2]])\n      expect(CALLS).to eq([[1, 1], [1, 2]])\n    end\n  end\n\n  context \"memoizing inexistent method\" do\n    subject(:klass) do\n      Class.new do\n        include Memery\n        memoize :foo\n      end\n    end\n\n    specify do\n      expect { klass }.to raise_error(ArgumentError, /Method foo is not defined/)\n    end\n  end\n\n  context \"Forwardable\" do\n    subject(:e) { E.new }\n\n    specify do\n      values = [e.m, e.m, e.m]\n      expect(values).to eq([:m, :m, :m])\n      expect(CALLS).to eq([:m])\n    end\n  end\n\n  context \"without hashed arguments\" do\n    before { Memery.use_hashed_arguments = false }\n\n    context \"methods without args\" do\n      specify do\n        values = [ a.m, a.m_nil, a.m, a.m_nil ]\n        expect(values).to eq([:m, nil, :m, nil])\n        expect(CALLS).to eq([:m, nil])\n      end\n    end\n\n    context \"method with args\" do\n      specify do\n        values = [ a.m_args(1, 1), a.m_args(1, 1), a.m_args(1, 2) ]\n        expect(values).to eq([[1, 1], [1, 1], [1, 2]])\n        expect(CALLS).to eq([[1, 1], [1, 2]])\n      end\n    end\n  end\n\n  describe \":condition option\" do\n    before do\n      a.environment = environment\n    end\n\n    context \"returns true\" do\n      let(:environment) { \"production\" }\n\n      specify do\n        values = [ a.m_condition, a.m_nil, a.m_condition, a.m_nil ]\n        expect(values).to eq([:m_condition, nil, :m_condition, nil])\n        expect(CALLS).to eq([:m_condition, nil])\n      end\n    end\n\n    context \"returns false\" do\n      let(:environment) { \"development\" }\n\n      specify do\n        values = [ a.m_condition, a.m_nil, a.m_condition, a.m_nil ]\n        expect(values).to eq([:m_condition, nil, :m_condition, nil])\n        expect(CALLS).to eq([:m_condition, nil, :m_condition])\n      end\n    end\n  end\n\n  describe \":ttl option\" do\n    specify do\n      values = [ a.m_ttl(1, 1), a.m_ttl(1, 1), a.m_ttl(1, 2) ]\n      expect(values).to eq([[1, 1], [1, 1], [1, 2]])\n      expect(CALLS).to eq([[1, 1], [1, 2]])\n\n      allow(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC)\n        .and_wrap_original { |m, *args| m.call(*args) + 5 }\n\n      values = [ a.m_ttl(1, 1), a.m_ttl(1, 1), a.m_ttl(1, 2) ]\n      expect(values).to eq([[1, 1], [1, 1], [1, 2]])\n      expect(CALLS).to eq([[1, 1], [1, 2], [1, 1], [1, 2]])\n    end\n\n    context \"returns false\" do\n      let(:environment) { \"development\" }\n\n      specify do\n        values = [ a.m_condition, a.m_nil, a.m_condition, a.m_nil ]\n        expect(values).to eq([:m_condition, nil, :m_condition, nil])\n        expect(CALLS).to eq([:m_condition, nil, :m_condition])\n      end\n    end\n  end\n\n  describe \"with multiple methods\" do\n    let(:h) { H.new }\n\n    specify do\n      values = [h.m, h.n, h.m, h.n]\n      expect(values).to eq([:m, :n, :m, :n])\n      expect(CALLS).to eq([:m, :n])\n    end\n\n    specify do\n      values = [h.x, h.y, h.x, h.y]\n      expect(values).to eq([:x, :y, :x, :y])\n      expect(CALLS).to eq([:x, :y])\n    end\n\n    specify do\n      expect(unmemoized_class.memoize(:x, :y, ttl: 3)).to eq([:x, :y])\n    end\n  end\n\n  describe \".memoize return value\" do\n    specify do\n      expect(unmemoized_class.memoize(:x)).to eq(:x)\n      expect(unmemoized_class.memoize(:m, ttl: 3)).to eq(:m)\n      expect(unmemoized_class.memoize(:a, condition: -> { 1 == 2 })).to eq(:a)\n    end\n\n    specify do\n      expect(unmemoized_class.memoize(:x, :y)).to eq([:x, :y])\n      expect(unmemoized_class.memoize(:m, :n, ttl: 3)).to eq([:m, :n])\n      expect(unmemoized_class.memoize(:a, :b, condition: -> { 1 == 2 })).to eq([:a, :b])\n    end\n  end\n\n  describe \".memoized?\" do\n    subject { object.memoized?(method_name) }\n\n    context \"class without memoized methods\" do\n      let(:object) { F }\n      let(:method_name) { :m }\n\n      it { is_expected.to be false }\n    end\n\n    shared_examples \"works correctly\" do\n      context \"public memoized method\" do\n        let(:method_name) { :m }\n\n        it { is_expected.to be true }\n      end\n\n      context \"memoize is on a different line\" do\n        let(:method_name) { :m_different_line }\n\n        it { is_expected.to be true }\n      end\n\n      context \"private memoized method\" do\n        let(:method_name) { :m_private }\n\n        it { is_expected.to be true }\n      end\n\n      context \"non-memoized method\" do\n        let(:method_name) { :not_memoized }\n\n        it { is_expected.to be false }\n      end\n\n      context \"standard class method\" do\n        let(:method_name) { :constants }\n\n        it { is_expected.to be false }\n      end\n\n      context \"standard instance method\" do\n        let(:method_name) { :to_s }\n\n        it { is_expected.to be false }\n      end\n    end\n\n    context \"class\" do\n      let(:object) { A }\n\n      it_behaves_like \"works correctly\"\n    end\n\n    context \"module\" do\n      let(:object) { M }\n\n      it_behaves_like \"works correctly\"\n    end\n  end\nend\n"
  },
  {
    "path": "spec/spec_helper.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"simplecov\"\nrequire \"simplecov-lcov\"\n\nSimpleCov::Formatter::LcovFormatter.config do |config|\n  config.report_with_single_file = true\n  config.single_report_path = \"coverage/lcov.info\"\nend\n\nSimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([\n  SimpleCov::Formatter::HTMLFormatter,\n  SimpleCov::Formatter::LcovFormatter,\n])\n\nSimpleCov.start do\n  enable_coverage(:branch)\n  minimum_coverage(line: 100, branch: 100)\nend\n\nrequire \"memery\"\nrequire \"active_support/concern\"\n\nRSpec.configure do |config|\n  # Enable flags like --only-failures and --next-failure\n  config.example_status_persistence_file_path = \".rspec_status\"\n\n  # Disable RSpec exposing methods globally on `Module` and `main`\n  config.disable_monkey_patching!\n\n  config.expect_with :rspec do |c|\n    c.syntax = :expect\n  end\nend\n"
  }
]