TylerRick / rack_attack_admin

A Rack::Attack admin dashboard
MIT License
7 stars 9 forks source link

Doesn't work in Rails 4.2: LoadError: cannot load such file -- active_support/duration/iso8601_serializer #2

Open Nowaker opened 4 years ago

Nowaker commented 4 years ago

Fails during bundling phase:

     9: from /Users/nowaker/.rvm/gems/ruby-2.6.3/gems/rack_attack_admin-0.1.2/lib/rack_attack_admin.rb:3:in `<top (required)>'
     8: from /Users/nowaker/.rvm/gems/ruby-2.6.3/gems/activesupport-4.2.11.1/lib/active_support/dependencies.rb:274:in `require'
     7: from /Users/nowaker/.rvm/gems/ruby-2.6.3/gems/activesupport-4.2.11.1/lib/active_support/dependencies.rb:240:in `load_dependency'
     6: from /Users/nowaker/.rvm/gems/ruby-2.6.3/gems/activesupport-4.2.11.1/lib/active_support/dependencies.rb:274:in `block in require'
     5: from /Users/nowaker/.rvm/gems/ruby-2.6.3/gems/activesupport-4.2.11.1/lib/active_support/dependencies.rb:274:in `require'
     4: from /Users/nowaker/.rvm/gems/ruby-2.6.3/gems/activesupport-duration-human_string-0.1.1/lib/active_support/duration/human_string.rb:2:in `<top (required)>'
     3: from /Users/nowaker/.rvm/gems/ruby-2.6.3/gems/activesupport-4.2.11.1/lib/active_support/dependencies.rb:274:in `require'
     2: from /Users/nowaker/.rvm/gems/ruby-2.6.3/gems/activesupport-4.2.11.1/lib/active_support/dependencies.rb:240:in `load_dependency'
     1: from /Users/nowaker/.rvm/gems/ruby-2.6.3/gems/activesupport-4.2.11.1/lib/active_support/dependencies.rb:274:in `block in require'
/Users/nowaker/.rvm/gems/ruby-2.6.3/gems/activesupport-4.2.11.1/lib/active_support/dependencies.rb:274:in `require': cannot load such file -- active_support/duration/iso8601_serializer (LoadError)

As per https://apidock.com/rails/ActiveSupport/Duration/ISO8601Serializer, this class was introduced in Rails 5. The gemspec itself depends on Rails >= 4.2.

Nowaker commented 4 years ago

Monkey patches to make the gem work in Rails 4.2:

config.ru:

require_relative 'config/lib/rails5_iso8601_serializer.rb'

config/initializers/rack_attack_admin.rb:

require 'active_support/duration'
class ActiveSupport::Duration
  # Copied from https://github.com/rails/rails/blob/dcd36ffe1acf922429bf185206749693c5df5f8f/activesupport/lib/active_support/duration.rb#L108
  SECONDS_PER_MINUTE = 60
  SECONDS_PER_HOUR   = 3600
  SECONDS_PER_DAY    = 86400
  SECONDS_PER_WEEK   = 604800
  SECONDS_PER_MONTH  = 2629746  # 1/12 of a gregorian year
  SECONDS_PER_YEAR   = 31556952 # length of a gregorian year (365.2425 days)

  PARTS_IN_SECONDS = {
    seconds: 1,
    minutes: SECONDS_PER_MINUTE,
    hours:   SECONDS_PER_HOUR,
    days:    SECONDS_PER_DAY,
    weeks:   SECONDS_PER_WEEK,
    months:  SECONDS_PER_MONTH,
    years:   SECONDS_PER_YEAR
  }.freeze

  PARTS = [:years, :months, :weeks, :days, :hours, :minutes, :seconds].freeze  

  # Copied from https://github.com/rails/rails/blob/dcd36ffe1acf922429bf185206749693c5df5f8f/activesupport/lib/active_support/duration.rb#L183
  def self.build(value)
    unless value.is_a?(::Numeric)
      raise TypeError, "can't build an #{self.name} from a #{value.class.name}"
    end

    parts = {}
    remainder = value.round(9)

    PARTS.each do |part|
      unless part == :seconds
        part_in_seconds = PARTS_IN_SECONDS[part]
        parts[part] = remainder.div(part_in_seconds)
        remainder %= part_in_seconds
      end
    end unless value == 0

    parts[:seconds] = remainder

    new(value, parts)
  end
end

config/lib/rails5_iso8601_serializer.rb:

# Pretend active_support/duration/iso8601_serializer.rb is loaded
$LOADED_FEATURES << 'active_support/duration/iso8601_serializer.rb'

# Copied from: https://github.com/rails/rails/blob/726f86358d5c2bdf4317b9f8f08b30a45fd326a2/activesupport/lib/active_support/duration/iso8601_serializer.rb

# frozen_string_literal: true

require "active_support/core_ext/object/blank"

module ActiveSupport
  class Duration
    # Serializes duration to string according to ISO 8601 Duration format.
    class ISO8601Serializer # :nodoc:
      DATE_COMPONENTS = %i(years months days)

      def initialize(duration, precision: nil)
        @duration = duration
        @precision = precision
      end

      # Builds and returns output string.
      def serialize
        parts = normalize
        return "PT0S" if parts.empty?

        output = +"P"
        output << "#{parts[:years]}Y"   if parts.key?(:years)
        output << "#{parts[:months]}M"  if parts.key?(:months)
        output << "#{parts[:days]}D"    if parts.key?(:days)
        output << "#{parts[:weeks]}W"   if parts.key?(:weeks)
        time = +""
        time << "#{parts[:hours]}H"     if parts.key?(:hours)
        time << "#{parts[:minutes]}M"   if parts.key?(:minutes)
        if parts.key?(:seconds)
          time << "#{sprintf(@precision ? "%0.0#{@precision}f" : '%g', parts[:seconds])}S"
        end
        output << "T#{time}" unless time.empty?
        output
      end

      private
        # Return pair of duration's parts and whole duration sign.
        # Parts are summarized (as they can become repetitive due to addition, etc).
        # Zero parts are removed as not significant.
        # If all parts are negative it will negate all of them and return minus as a sign.
        def normalize
          parts = @duration.parts.each_with_object(Hash.new(0)) do |(k, v), p|
            p[k] += v  unless v.zero?
          end

          # Convert weeks to days and remove weeks if mixed with date parts
          if week_mixed_with_date?(parts)
            parts[:days] += parts.delete(:weeks) * SECONDS_PER_WEEK / SECONDS_PER_DAY
          end

          parts
        end

        def week_mixed_with_date?(parts)
          parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any?
        end
    end
  end
end