<commit_before>require "rate_limiter/version"

module RateLimiter
  class Middleware
    DEFAULT_BLOCK = lambda { |env| Rack::Request.new(env).ip }

    def initialize(app, options = {}, &block)
      @app = app
      @options = options.dup
      @options[:block] = block || DEFAULT_BLOCK
      @options[:store] ||= HashStore.new
      @options[:limit] ||= 60
    end

    def call(env)
      Request.new(@app, @options, env).perform
    end
  end

  class Request
    attr_reader :app, :options, :env

    def initialize(app, options, env)
      @app, @options, @env = app, options, env
    end

    def perform
      if apply_rate_limit?
        if new_client? || expired?
          reset_limit
        end
        if exceeded?
          response_for_limit_exceeded
        else
          call_app_with_limit
        end
      else
        call_app
      end
    end

    protected

    def apply_rate_limit?
      ! key.nil?
    end

    def key
      @key ||= options[:block].call(env)
    end

    def new_client?
      data_from_store.nil?
    end

    def expired?
      expires_at < Time.now
    end

    def exceeded?
      remaining_limit == 0
    end

    def expires_at
      @expires_at ||= data_from_store && data_from_store[:expires_at]
    end

    attr_writer :expires_at

    def remaining_limit
      @remaining_limit ||= data_from_store && data_from_store[:remaining_limit]
    end

    attr_writer :remaining_limit

    def total_limit
      options[:limit]
    end

    def reset_limit
      self.remaining_limit = total_limit
      self.expires_at = Time.now + 3600
    end

    def data_from_store
      options[:store].get(key)
    end

    def store_data
      options[:store].set(key, { expires_at: expires_at, remaining_limit: remaining_limit })
    end

    def response_for_limit_exceeded
      [429, {}, ""]
    end
<commit_msg>Make the reset time configurable<commit_after>require "rate_limiter/version"

module RateLimiter
  class Middleware
    DEFAULT_BLOCK = lambda { |env| Rack::Request.new(env).ip }

    def initialize(app, options = {}, &block)
      @app = app
      @options = options.dup
      @options[:block] = block || DEFAULT_BLOCK
      @options[:store] ||= HashStore.new
      @options[:limit] ||= 60
      @options[:reset_in] ||= 3600
    end

    def call(env)
      Request.new(@app, @options, env).perform
    end
  end

  class Request
    attr_reader :app, :options, :env

    def initialize(app, options, env)
      @app, @options, @env = app, options, env
    end

    def perform
      if apply_rate_limit?
        if new_client? || expired?
          reset_limit
        end
        if exceeded?
          response_for_limit_exceeded
        else
          call_app_with_limit
        end
      else
        call_app
      end
    end

    protected

    def apply_rate_limit?
      ! key.nil?
    end

    def key
      @key ||= options[:block].call(env)
    end

    def new_client?
      data_from_store.nil?
    end

    def expired?
      expires_at < Time.now
    end

    def exceeded?
      remaining_limit == 0
    end

    def expires_at
      @expires_at ||= data_from_store && data_from_store[:expires_at]
    end

    attr_writer :expires_at

    def remaining_limit
      @remaining_limit ||= data_from_store && data_from_store[:remaining_limit]
    end

    attr_writer :remaining_limit

    def total_limit
      options[:limit]
    end

    def reset_limit
      self.remaining_limit = total_limit
      self.expires_at = Time.now + options[:reset_in]
    end

    def data_from_store
      options[:store].get(key)
    end

    def store_data
      options[:store].set(key, { expires_at: expires_at, remaining_limit: remaining_limit })
    end

    def response_for_limit_exceeded
      [429, {}, ""]
    end

{'fraction_non_alphanumeric': 0.08874801901743265, 'fraction_numerical': 0.005282620179609086, 'mean_word_length': 2.2702936096718482, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2838533', 'n_tokens_mistral': 1261, 'n_tokens_neox': 1188, 'n_words': 280}