summaryrefslogtreecommitdiff
path: root/lib/hashie/rash.rb
blob: 1caa4c6cf60352e08a6c5ac531ca083ab99a0362 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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

    #
    # 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)
          if match
            @regex_counts[regex] += 1
            value = @hash[regex]
            if value.respond_to? :call
              yield value.call(match)
            else
              yield value
            end
          end
        end

      when Integer
        # see if any of the ranges match the integer
        @ranges.each do |range|
          yield @hash[range] if range.include? query
        end

      when Regexp
        # Reverse operation: `rash[/regexp/]` returns all the hash's string keys which match 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)
    end

    private

    def optimize_if_necessary!
      if (@lookups += 1) >= @optimize_every
        @regexes = @regex_counts.sort_by { |_, count| -count }.map { |regex, _| regex }
        @lookups = 0
      end
    end
  end
end