lib/hashie/rash.rb
module Hashie
#
# Rash is a Hash whose keys can be Regexps, or Ranges, which will
# match many input keys.
#
# A good use case for this class is routing URLs in a web framework.
# The Rash's keys match URL patterns, and the values specify actions
# which can handle the URL. When the Rash's value is proc, the proc
# will be automatically called with the regexp's matched groups as
# block arguments.
#
# Usage example:
#
# greeting = Hashie::Rash.new( /^Mr./ => "Hello sir!", /^Mrs./ => "Evening, madame." )
# greeting["Mr. Steve Austin"] #=> "Hello sir!"
# greeting["Mrs. Steve Austin"] #=> "Evening, madame."
#
# Note: The Rash is automatically optimized every 500 accesses
# (Regexps get sorted by how often they get matched).
# If this is too low or too high, you can tune it by
# setting: `rash.optimize_every = n`
#
class Rash
attr_accessor :optimize_every
def initialize(initial = {})
@hash = {}
@regexes = []
@ranges = []
@regex_counts = Hash.new(0)
@optimize_every = 500
@lookups = 0
update(initial)
end
def update(other)
other.each do |key, value|
self[key] = value
end
self
end
def []=(key, value)
case key
when Regexp
# key = normalize_regex(key) # this used to just do: /#{regexp}/
@regexes << key
when Range
@ranges << key
end
@hash[key] = value
end
#
# Return the first thing that matches the key.
#
def [](key)
all(key).first
end
#
# Raise (or yield) unless something matches the key.
#
def fetch(*args)
raise ArgumentError, "Expected 1-2 arguments, got #{args.length}" \
unless (1..2).cover?(args.length)
key, default = args
all(key) do |value|
return value
end
if block_given?
yield key
elsif default
default
else
raise KeyError, "key not found: #{key.inspect}"
end
end
#
# Return everything that matches the query.
#
def all(query)
return to_enum(:all, query) unless block_given?
if @hash.include? query
yield @hash[query]
return
end
case query
when String
optimize_if_necessary!
# see if any of the regexps match the string
@regexes.each do |regex|
match = regex.match(query)
next unless match
@regex_counts[regex] += 1
value = @hash[regex]
if value.respond_to? :call
yield value.call(match)
else
yield value
end
end
when Numeric
# see if any of the ranges match the integer
@ranges.each do |range|
yield @hash[range] if range.cover? query
end
when Regexp
# Reverse operation: `rash[/regexp/]` returns all string keys matching the regexp
@hash.each do |key, val|
yield val if key.is_a?(String) && query =~ key
end
end
end
def method_missing(*args, &block)
@hash.send(*args, &block) || super
end
def respond_to_missing?(method_name, _include_private = false)
@hash.respond_to?(method_name)
end
private
def optimize_if_necessary!
return unless (@lookups += 1) >= @optimize_every
@regexes = @regexes.sort_by { |regex| -@regex_counts[regex] }
@lookups = 0
end
end
end