summaryrefslogtreecommitdiff
path: root/lib/hashie/extensions/indifferent_access.rb
blob: c2c788884187d20dee21d891fd73faffe16f04d4 (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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
module Hashie
  module Extensions
    # IndifferentAccess gives you the ability to not care
    # whether your hash has string or symbol keys. Made famous
    # in Rails for accessing query and POST parameters, this
    # is a handy tool for making sure your hash has maximum
    # utility.
    #
    # One unique feature of this mixin is that it will recursively
    # inject itself into sub-hash instances without modifying
    # the actual class of the sub-hash.
    #
    # @example
    #   class MyHash < Hash
    #     include Hashie::Extensions::MergeInitializer
    #     include Hashie::Extensions::IndifferentAccess
    #   end
    #
    #   h = MyHash.new(:foo => 'bar', 'baz' => 'blip')
    #   h['foo'] # => 'bar'
    #   h[:foo]  # => 'bar'
    #   h[:baz]  # => 'blip'
    #   h['baz'] # => 'blip'
    #
    module IndifferentAccess
      include Hashie::Extensions::RubyVersionCheck

      def self.included(base)
        Hashie::Extensions::Dash::IndifferentAccess.maybe_extend(base)

        base.class_eval do
          alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
          alias_method :[]=, :indifferent_writer
          alias_method :store, :indifferent_writer
          %w[default update replace fetch delete key? values_at].each do |m|
            alias_method "regular_#{m}", m unless method_defined?("regular_#{m}")
            alias_method m, "indifferent_#{m}"
          end

          %w[include? member? has_key?].each do |key_alias|
            alias_method key_alias, :indifferent_key?
          end

          class << self
            def [](*)
              super.convert!
            end

            def try_convert(*)
              (hash = super) && self[hash]
            end
          end
        end
      end

      # This will inject indifferent access into an instance of
      # a hash without modifying the actual class. This is what
      # allows IndifferentAccess to spread to sub-hashes.
      def self.inject!(hash)
        (class << hash; self; end).send :include, IndifferentAccess
        hash.convert!
      end

      # Injects indifferent access into a duplicate of the hash
      # provided. See #inject!
      def self.inject(hash)
        inject!(hash.dup)
      end

      def convert_key(key)
        key.to_s
      end

      # Iterates through the keys and values, reconverting them to
      # their proper indifferent state. Used when IndifferentAccess
      # is injecting itself into member hashes.
      def convert!
        keys.each do |k| # rubocop:disable Performance/HashEachMethods
          indifferent_writer k, regular_delete(k)
        end
        self
      end

      def indifferent_value(value)
        if hash_lacking_indifference?(value)
          IndifferentAccess.inject!(value)
        elsif value.is_a?(::Array)
          value.replace(value.map { |e| indifferent_value(e) })
        else
          value
        end
      end

      def indifferent_default(key = nil)
        return self[convert_key(key)] if key?(key)
        regular_default(key)
      end

      def indifferent_update(other_hash)
        return regular_update(other_hash) if hash_with_indifference?(other_hash)
        other_hash.each_pair do |k, v|
          self[k] = v
        end
      end

      def indifferent_writer(key, value)
        regular_writer convert_key(key), indifferent_value(value)
      end

      def indifferent_fetch(key, *args, &block)
        regular_fetch convert_key(key), *args, &block
      end

      def indifferent_delete(key)
        regular_delete convert_key(key)
      end

      def indifferent_key?(key)
        regular_key? convert_key(key)
      end

      def indifferent_values_at(*indices)
        indices.map { |i| self[i] }
      end

      def indifferent_access?
        true
      end

      def indifferent_replace(other_hash)
        (keys - other_hash.keys).each { |key| delete(key) }
        other_hash.each { |key, value| self[key] = value }
        self
      end

      def merge(*args)
        result = super
        return IndifferentAccess.inject!(result) if hash_lacking_indifference?(result)
        result.convert!
      end

      def merge!(*)
        super.convert!
      end

      def to_hash
        {}.tap do |result|
          each_pair { |key, value| result[key] = value }

          if default_proc
            result.default_proc = default_proc
          else
            result.default = default
          end
        end
      end

      with_minimum_ruby('2.5.0') do
        def slice(*keys)
          string_keys = keys.map { |key| convert_key(key) }
          super(*string_keys)
        end
      end

      with_minimum_ruby('3.0.0') do
        def except(*keys)
          string_keys = keys.map { |key| convert_key(key) }
          super(*string_keys)
        end
      end

      protected

      def hash_lacking_indifference?(other)
        other.is_a?(::Hash) &&
          !(other.respond_to?(:indifferent_access?) &&
            other.indifferent_access?)
      end

      def hash_with_indifference?(other)
        other.is_a?(::Hash) &&
          other.respond_to?(:indifferent_access?) &&
          other.indifferent_access?
      end
    end
  end
end