├── .gitignore
├── .travis.yml
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── lib
├── symbolized.rb
└── symbolized
│ ├── core_ext
│ ├── hash
│ │ ├── keys.rb
│ │ ├── reverse_merge.rb
│ │ └── symbolized_hash.rb
│ └── object
│ │ ├── deep_dup.rb
│ │ └── duplicable.rb
│ ├── symbolized_hash.rb
│ └── version.rb
├── symbolized.gemspec
└── test
├── core_ext
└── hash_ext_test.rb
└── test_helpers.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | /.config
4 | /coverage/
5 | /InstalledFiles
6 | /pkg/
7 | /spec/reports/
8 | /test/tmp/
9 | /test/version_tmp/
10 | /tmp/
11 |
12 | ## Specific to RubyMotion:
13 | .dat*
14 | .repl_history
15 | build/
16 |
17 | ## Documentation cache and generated files:
18 | /.yardoc/
19 | /_yardoc/
20 | /doc/
21 | /rdoc/
22 |
23 | ## Environment normalisation:
24 | /.bundle/
25 | /vendor/bundle
26 | /lib/bundler/man/
27 |
28 | # for a library or gem, you might want to ignore these files since the code is
29 | # intended to run in multiple environments; otherwise, check them in:
30 | Gemfile.lock
31 | .ruby-version
32 | .ruby-gemset
33 |
34 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
35 | .rvmrc
36 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 1.9.3
4 | - 2.0.0
5 | - 2.1.0
6 | - 2.2.0
7 | - 2.2.2
8 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
4 |
5 | gem 'rake'
6 |
7 | group :test do
8 | gem 'minitest', '~> 5.1'
9 | end
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Tamer Shlash
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Symbolized
2 |
3 | [](https://travis-ci.org/TamerShlash/symbolized)
4 |
5 | Symbolized provides a Hash with indifferent access, but with keys stored internally as symbols.
6 | This is particularly useful when you have a very big amount of hashes that share the same keys,
7 | and it may become inefficient to keep all these identical keys as strings. An example of this
8 | case is when you have data processing pipelines that process millions of hashes with the same
9 | keys.
10 |
11 | ## Installation
12 |
13 | You can either install it manually:
14 |
15 | % [sudo] gem install symbolized
16 |
17 | Or include it in your Gemfile:
18 |
19 | gem 'symbolized'
20 |
21 | And then run `bundle install`.
22 |
23 | ## Usage
24 |
25 | ```ruby
26 | require 'symbolized'
27 |
28 | # You can create a SymbolizedHash directly:
29 |
30 | symbolized_hash = SymbolizedHash.new
31 | symbolized_hash['a'] = 'b'
32 | symbolized_hash['a'] #=> 'b'
33 | symbolized_hash[:a] #=> 'b'
34 | symbolized_hash.keys #=> [:a]
35 |
36 | # Or initialize it with a normal hash:
37 |
38 | symbolized_hash = SymbolizedHash.new({'a' => 'b'})
39 | symbolized_hash['a'] #=> 'b'
40 | symbolized_hash[:a] #=> 'b'
41 | symbolized_hash.keys #=> [:a]
42 |
43 | # Or use the Hash#to_symbolized_hash core extension:
44 |
45 | h = { 'a' => 'b' }
46 | h['a'] #=> 'b'
47 | h[:a] #=> nil
48 | h.keys #=> ['a']
49 |
50 | symbolized_hash = h.to_symbolized_hash
51 | symbolized_hash['a'] #=> 'b'
52 | symbolized_hash[:a] #=> 'b'
53 | symbolized_hash.keys #=> [:a]
54 |
55 | ```
56 |
57 | The gem provides almost the same methods and functionality provided by ActiveSupport's `HashWithIndifferentAccess`, while storing keys internally as Symbols.
58 |
59 | ## `ActiveSupport` Compatibility
60 |
61 | This gem is built with intent to be as much as possible compatible with ActiveSupport. You can include both `Symbolized` and `ActiveSupport`, and you are guaranteed to get ActiveSupport functionality and core extensions, and still have `Symbolized` core extension and class.
62 |
63 | ## Testing
64 |
65 | Checkout [travis.yml](.travis.yml) to see which Ruby versions the gem has been tested against. Alternatively, if you want to test it yourself, you can clone the repo, run `bundle install` and then run `rake test`.
66 |
67 | ## Suggestions, Discussions and Issues
68 |
69 | Please propose suggestions, open discussions, or report bugs and issues [here](https://github.com/TamerShlash/symbolized/issues).
70 |
71 | ## Contributing
72 |
73 | 1. Fork the [repo](https://github.com/TamerShlash/symbolized)
74 | 2. Create your feature branch (`git checkout -b my-new-feature`)
75 | 3. Commit your changes (`git commit -am 'Add some feature'`)
76 | 4. Push to the branch (`git push origin my-new-feature`)
77 | 5. Create a new Pull Request
78 |
79 | ## Credits
80 |
81 | The current code of this gem is heavily based on [ActiveSupport 4.2 HashWithIndifferentAccess](https://github.com/rails/rails/tree/4-2-stable/activesupport). Some parts are direct clones, other parts have been modified and/or refactored.
82 |
83 | ## License
84 |
85 | Copyright (c) 2015 [Tamer Shlash](https://github.com/TamerShlash) ([@TamerShlash](https://twitter.com/TamerShlash)). Released under the MIT [License](LICENSE).
86 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rake/testtask'
2 |
3 | Rake::TestTask.new do |t|
4 | t.libs << 'test'
5 | t.test_files = FileList['test/core_ext/hash_ext_test.rb']
6 | end
7 |
8 | desc "Run tests"
9 | task :default => :test
10 |
--------------------------------------------------------------------------------
/lib/symbolized.rb:
--------------------------------------------------------------------------------
1 | require 'symbolized/symbolized_hash'
2 | require 'symbolized/core_ext/hash/symbolized_hash'
3 |
--------------------------------------------------------------------------------
/lib/symbolized/core_ext/hash/keys.rb:
--------------------------------------------------------------------------------
1 | class Hash
2 | # Returns a new hash with all keys converted using the block operation.
3 | #
4 | # hash = { name: 'Rob', age: '28' }
5 | #
6 | # hash.transform_keys{ |key| key.to_s.upcase }
7 | # # => {"NAME"=>"Rob", "AGE"=>"28"}
8 | def transform_keys
9 | return enum_for(:transform_keys) unless block_given?
10 | result = self.class.new
11 | each_key do |key|
12 | result[yield(key)] = self[key]
13 | end
14 | result
15 | end
16 |
17 | # Destructively convert all keys using the block operations.
18 | # Same as transform_keys but modifies +self+.
19 | def transform_keys!
20 | return enum_for(:transform_keys!) unless block_given?
21 | keys.each do |key|
22 | self[yield(key)] = delete(key)
23 | end
24 | self
25 | end
26 |
27 | # Returns a new hash with all keys converted to strings.
28 | #
29 | # hash = { name: 'Rob', age: '28' }
30 | #
31 | # hash.stringify_keys
32 | # # => {"name"=>"Rob", "age"=>"28"}
33 | def stringify_keys
34 | transform_keys{ |key| key.to_s }
35 | end
36 |
37 | # Destructively convert all keys to strings. Same as
38 | # +stringify_keys+, but modifies +self+.
39 | def stringify_keys!
40 | transform_keys!{ |key| key.to_s }
41 | end
42 |
43 | # Returns a new hash with all keys converted to symbols, as long as
44 | # they respond to +to_sym+.
45 | #
46 | # hash = { 'name' => 'Rob', 'age' => '28' }
47 | #
48 | # hash.symbolize_keys
49 | # # => {:name=>"Rob", :age=>"28"}
50 | def symbolize_keys
51 | transform_keys{ |key| key.to_sym rescue key }
52 | end
53 | alias_method :to_options, :symbolize_keys
54 |
55 | # Destructively convert all keys to symbols, as long as they respond
56 | # to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
57 | def symbolize_keys!
58 | transform_keys!{ |key| key.to_sym rescue key }
59 | end
60 | alias_method :to_options!, :symbolize_keys!
61 |
62 | # Validate all keys in a hash match *valid_keys, raising
63 | # ArgumentError on a mismatch.
64 | #
65 | # Note that keys are treated differently than HashWithIndifferentAccess,
66 | # meaning that string and symbol keys will not match.
67 | #
68 | # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
69 | # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
70 | # { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing
71 | def assert_valid_keys(*valid_keys)
72 | valid_keys.flatten!
73 | each_key do |k|
74 | unless valid_keys.include?(k)
75 | raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
76 | end
77 | end
78 | end
79 |
80 | # Returns a new hash with all keys converted by the block operation.
81 | # This includes the keys from the root hash and from all
82 | # nested hashes and arrays.
83 | #
84 | # hash = { person: { name: 'Rob', age: '28' } }
85 | #
86 | # hash.deep_transform_keys{ |key| key.to_s.upcase }
87 | # # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
88 | def deep_transform_keys(&block)
89 | _deep_transform_keys_in_object(self, &block)
90 | end
91 |
92 | # Destructively convert all keys by using the block operation.
93 | # This includes the keys from the root hash and from all
94 | # nested hashes and arrays.
95 | def deep_transform_keys!(&block)
96 | _deep_transform_keys_in_object!(self, &block)
97 | end
98 |
99 | # Returns a new hash with all keys converted to strings.
100 | # This includes the keys from the root hash and from all
101 | # nested hashes and arrays.
102 | #
103 | # hash = { person: { name: 'Rob', age: '28' } }
104 | #
105 | # hash.deep_stringify_keys
106 | # # => {"person"=>{"name"=>"Rob", "age"=>"28"}}
107 | def deep_stringify_keys
108 | deep_transform_keys{ |key| key.to_s }
109 | end
110 |
111 | # Destructively convert all keys to strings.
112 | # This includes the keys from the root hash and from all
113 | # nested hashes and arrays.
114 | def deep_stringify_keys!
115 | deep_transform_keys!{ |key| key.to_s }
116 | end
117 |
118 | # Returns a new hash with all keys converted to symbols, as long as
119 | # they respond to +to_sym+. This includes the keys from the root hash
120 | # and from all nested hashes and arrays.
121 | #
122 | # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
123 | #
124 | # hash.deep_symbolize_keys
125 | # # => {:person=>{:name=>"Rob", :age=>"28"}}
126 | def deep_symbolize_keys
127 | deep_transform_keys{ |key| key.to_sym rescue key }
128 | end
129 |
130 | # Destructively convert all keys to symbols, as long as they respond
131 | # to +to_sym+. This includes the keys from the root hash and from all
132 | # nested hashes and arrays.
133 | def deep_symbolize_keys!
134 | deep_transform_keys!{ |key| key.to_sym rescue key }
135 | end
136 |
137 | private
138 | # support methods for deep transforming nested hashes and arrays
139 | def _deep_transform_keys_in_object(object, &block)
140 | case object
141 | when Hash
142 | object.each_with_object({}) do |(key, value), result|
143 | result[yield(key)] = _deep_transform_keys_in_object(value, &block)
144 | end
145 | when Array
146 | object.map {|e| _deep_transform_keys_in_object(e, &block) }
147 | else
148 | object
149 | end
150 | end
151 |
152 | def _deep_transform_keys_in_object!(object, &block)
153 | case object
154 | when Hash
155 | object.keys.each do |key|
156 | value = object.delete(key)
157 | object[yield(key)] = _deep_transform_keys_in_object!(value, &block)
158 | end
159 | object
160 | when Array
161 | object.map! {|e| _deep_transform_keys_in_object!(e, &block)}
162 | else
163 | object
164 | end
165 | end
166 | end unless defined? ActiveSupport
167 |
--------------------------------------------------------------------------------
/lib/symbolized/core_ext/hash/reverse_merge.rb:
--------------------------------------------------------------------------------
1 | class Hash
2 | # Merges the caller into +other_hash+. For example,
3 | #
4 | # options = options.reverse_merge(size: 25, velocity: 10)
5 | #
6 | # is equivalent to
7 | #
8 | # options = { size: 25, velocity: 10 }.merge(options)
9 | #
10 | # This is particularly useful for initializing an options hash
11 | # with default values.
12 | def reverse_merge(other_hash)
13 | other_hash.merge(self)
14 | end
15 |
16 | # Destructive +reverse_merge+.
17 | def reverse_merge!(other_hash)
18 | # right wins if there is no left
19 | merge!( other_hash ){|key,left,right| left }
20 | end
21 | alias_method :reverse_update, :reverse_merge!
22 | end unless defined? ActiveSupport
23 |
--------------------------------------------------------------------------------
/lib/symbolized/core_ext/hash/symbolized_hash.rb:
--------------------------------------------------------------------------------
1 | require 'symbolized'
2 |
3 | class Hash
4 | # Returns a Symbolized::SymbolizedHash out of its receiver:
5 | #
6 | # { 'a' => 1 }.to_symbolized_hash[:a] # => 1
7 | def to_symbolized_hash
8 | Symbolized::SymbolizedHash.new_from_hash_copying_default(self)
9 | end
10 |
11 | # Called when object is nested under an object that receives
12 | # #to_symbolized_hash. This method will be called on the current object
13 | # by the enclosing object and is aliased to #to_symbolized_hash by
14 | # default. Subclasses of Hash may overwrite this method to return +self+ if
15 | # converting to a Symbolized::SymbolizedHash would not be desirable.
16 | #
17 | # b = { 'b' => 1 }
18 | # { a: b }.with_indifferent_access['a'] # calls b.nested_under_indifferent_access
19 | # # => { :b => 1 }
20 | alias nested_under_symbolized_hash to_symbolized_hash
21 | end
22 |
--------------------------------------------------------------------------------
/lib/symbolized/core_ext/object/deep_dup.rb:
--------------------------------------------------------------------------------
1 | unless defined? ActiveSupport
2 | require 'symbolized/core_ext/object/duplicable'
3 |
4 | class Object
5 | # Returns a deep copy of object if it's duplicable. If it's
6 | # not duplicable, returns +self+.
7 | #
8 | # object = Object.new
9 | # dup = object.deep_dup
10 | # dup.instance_variable_set(:@a, 1)
11 | #
12 | # object.instance_variable_defined?(:@a) # => false
13 | # dup.instance_variable_defined?(:@a) # => true
14 | def deep_dup
15 | duplicable? ? dup : self
16 | end
17 | end
18 |
19 | class Array
20 | # Returns a deep copy of array.
21 | #
22 | # array = [1, [2, 3]]
23 | # dup = array.deep_dup
24 | # dup[1][2] = 4
25 | #
26 | # array[1][2] # => nil
27 | # dup[1][2] # => 4
28 | def deep_dup
29 | map { |it| it.deep_dup }
30 | end
31 | end
32 |
33 | class Hash
34 | # Returns a deep copy of hash.
35 | #
36 | # hash = { a: { b: 'b' } }
37 | # dup = hash.deep_dup
38 | # dup[:a][:c] = 'c'
39 | #
40 | # hash[:a][:c] # => nil
41 | # dup[:a][:c] # => "c"
42 | def deep_dup
43 | each_with_object(dup) do |(key, value), hash|
44 | hash[key.deep_dup] = value.deep_dup
45 | end
46 | end
47 | end
48 | end
--------------------------------------------------------------------------------
/lib/symbolized/core_ext/object/duplicable.rb:
--------------------------------------------------------------------------------
1 | #--
2 | # Most objects are cloneable, but not all. For example you can't dup +nil+:
3 | #
4 | # nil.dup # => TypeError: can't dup NilClass
5 | #
6 | # Classes may signal their instances are not duplicable removing +dup+/+clone+
7 | # or raising exceptions from them. So, to dup an arbitrary object you normally
8 | # use an optimistic approach and are ready to catch an exception, say:
9 | #
10 | # arbitrary_object.dup rescue object
11 | #
12 | # Rails dups objects in a few critical spots where they are not that arbitrary.
13 | # That rescue is very expensive (like 40 times slower than a predicate), and it
14 | # is often triggered.
15 | #
16 | # That's why we hardcode the following cases and check duplicable? instead of
17 | # using that rescue idiom.
18 | #++
19 | unless defined? ActiveSupport
20 | class Object
21 | # Can you safely dup this object?
22 | #
23 | # False for +nil+, +false+, +true+, symbol, number and BigDecimal(in 1.9.x) objects;
24 | # true otherwise.
25 | def duplicable?
26 | true
27 | end
28 | end
29 |
30 | class NilClass
31 | # +nil+ is not duplicable:
32 | #
33 | # nil.duplicable? # => false
34 | # nil.dup # => TypeError: can't dup NilClass
35 | def duplicable?
36 | false
37 | end
38 | end
39 |
40 | class FalseClass
41 | # +false+ is not duplicable:
42 | #
43 | # false.duplicable? # => false
44 | # false.dup # => TypeError: can't dup FalseClass
45 | def duplicable?
46 | false
47 | end
48 | end
49 |
50 | class TrueClass
51 | # +true+ is not duplicable:
52 | #
53 | # true.duplicable? # => false
54 | # true.dup # => TypeError: can't dup TrueClass
55 | def duplicable?
56 | false
57 | end
58 | end
59 |
60 | class Symbol
61 | # Symbols are not duplicable:
62 | #
63 | # :my_symbol.duplicable? # => false
64 | # :my_symbol.dup # => TypeError: can't dup Symbol
65 | def duplicable?
66 | false
67 | end
68 | end
69 |
70 | class Numeric
71 | # Numbers are not duplicable:
72 | #
73 | # 3.duplicable? # => false
74 | # 3.dup # => TypeError: can't dup Fixnum
75 | def duplicable?
76 | false
77 | end
78 | end
79 |
80 | require 'bigdecimal'
81 | class BigDecimal
82 | # Needed to support Ruby 1.9.x, as it doesn't allow dup on BigDecimal, instead
83 | # raises TypeError exception. Checking here on the runtime whether BigDecimal
84 | # will allow dup or not.
85 | begin
86 | BigDecimal.new('4.56').dup
87 |
88 | def duplicable?
89 | true
90 | end
91 | rescue TypeError
92 | # can't dup, so use superclass implementation
93 | end
94 | end
95 |
96 | class Method
97 | # Methods are not duplicable:
98 | #
99 | # method(:puts).duplicable? # => false
100 | # method(:puts).dup # => TypeError: allocator undefined for Method
101 | def duplicable?
102 | false
103 | end
104 | end
105 | end
106 |
--------------------------------------------------------------------------------
/lib/symbolized/symbolized_hash.rb:
--------------------------------------------------------------------------------
1 | require 'symbolized/core_ext/hash/keys'
2 | require 'symbolized/core_ext/hash/reverse_merge'
3 |
4 | module Symbolized
5 | # Implements a hash where keys :foo and "foo" are considered
6 | # to be the same.
7 | #
8 | # rgb = SymbolizedHash.new
9 | #
10 | # rgb[:black] = '#000000'
11 | # rgb[:black] # => '#000000'
12 | # rgb['black'] # => '#000000'
13 | #
14 | # rgb['white'] = '#FFFFFF'
15 | # rgb[:white] # => '#FFFFFF'
16 | # rgb['white'] # => '#FFFFFF'
17 | #
18 | # Internally strings are mapped to symbols when used as keys in the entire
19 | # writing interface (calling []=, merge, etc). This
20 | # mapping belongs to the public interface. For example, given:
21 | #
22 | # hash = SymbolizedHash.new('a' => 1)
23 | #
24 | # You are guaranteed that the key is returned as a symbol:
25 | #
26 | # hash.keys # => [:a]
27 | #
28 | # Technically other types of keys are accepted:
29 | #
30 | # hash = SymbolizedHash.new('a' => 1)
31 | # hash[0] = 0
32 | # hash # => { :a => 1, 0 => 0 }
33 | #
34 | # but this class is intended for use cases where strings or symbols are the
35 | # expected keys and it is convenient to understand both as the same. For
36 | # example, processing data throught a multi-step pipeline where steps can
37 | # be written by other people.
38 | #
39 | # Note that core extensions define Hash#to_symbolized_hash:
40 | #
41 | # rgb = { black: '#000000', 'white' => '#FFFFFF' }.to_symbolized_hash
42 | #
43 | # which may be handy.
44 | class SymbolizedHash < Hash
45 | # Returns +true+ so that Array#extract_options! finds members of
46 | # this class.
47 | def extractable_options?
48 | true
49 | end
50 |
51 | def symbolized
52 | dup
53 | end
54 |
55 | def nested_under_symbolized_hash
56 | self
57 | end
58 |
59 | def initialize(constructor = {})
60 | if constructor.respond_to?(:to_hash)
61 | super()
62 | update(constructor)
63 | else
64 | super(constructor)
65 | end
66 | end
67 |
68 | def default(key = nil)
69 | if key.is_a?(String) && include?(key = key.to_sym)
70 | self[key]
71 | else
72 | super
73 | end
74 | end
75 |
76 | def self.new_from_hash_copying_default(hash)
77 | hash = hash.to_hash
78 | new(hash).tap do |new_hash|
79 | new_hash.default = hash.default
80 | new_hash.default_proc = hash.default_proc if hash.default_proc
81 | end
82 | end
83 |
84 | def self.[](*args)
85 | new.merge!(Hash[*args])
86 | end
87 |
88 | alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
89 | alias_method :regular_update, :update unless method_defined?(:regular_update)
90 |
91 | # Assigns a new value to the hash:
92 | #
93 | # hash = SymbolizedHash.new
94 | # hash[:key] = 'value'
95 | #
96 | # This value can be later fetched using either +:key+ or +'key'+.
97 | def []=(key, value)
98 | regular_writer(convert_key(key), convert_value(value, for: :assignment))
99 | end
100 |
101 | alias_method :store, :[]=
102 |
103 | # Updates the receiver in-place, merging in the hash passed as argument:
104 | #
105 | # hash_1 = SymbolizedHash.new
106 | # hash_1['key'] = 'value'
107 | #
108 | # hash_2 = SymbolizedHash.new
109 | # hash_2['key'] = 'New Value!'
110 | #
111 | # hash_1.update(hash_2) # => { :key => 'New Value!' }
112 | #
113 | # The argument can be either a SymbolizedHash or a regular +Hash+.
114 | # In either case the merge respects the semantics of indifferent access.
115 | #
116 | # If the argument is a regular hash with keys +:key+ and +"key"+ only one
117 | # of the values end up in the receiver, but which one is unspecified.
118 | #
119 | # When given a block, the value for duplicated keys will be determined
120 | # by the result of invoking the block with the duplicated key, the value
121 | # in the receiver, and the value in +other_hash+. The rules for duplicated
122 | # keys follow the semantics of indifferent access:
123 | #
124 | # hash_1[:key] = 10
125 | # hash_2['key'] = 12
126 | # hash_1.update(hash_2) { |key, old, new| old + new } # => { :key => 22 }
127 | def update(other_hash)
128 | if other_hash.is_a? SymbolizedHash
129 | super(other_hash)
130 | else
131 | other_hash.to_hash.each_pair do |key, value|
132 | if block_given? && key?(key)
133 | value = yield(convert_key(key), self[key], value)
134 | end
135 | regular_writer(convert_key(key), convert_value(value))
136 | end
137 | self
138 | end
139 | end
140 |
141 | alias_method :merge!, :update
142 |
143 | # Checks the hash for a key matching the argument passed in:
144 | #
145 | # hash = SymbolizedHash.new
146 | # hash[:key] = 'value'
147 | # hash.key?(:key) # => true
148 | # hash.key?('key') # => true
149 | def key?(key)
150 | super(convert_key(key))
151 | end
152 |
153 | alias_method :include?, :key?
154 | alias_method :has_key?, :key?
155 | alias_method :member?, :key?
156 |
157 | # Same as Hash#fetch where the key passed as argument can be
158 | # either a string or a symbol:
159 | #
160 | # counters = SymbolizedHash.new
161 | # counters['foo'] = 1
162 | #
163 | # counters.fetch(:foo) # => 1
164 | # counters.fetch(:bar, 0) # => 0
165 | # counters.fetch(:bar) { |key| 0 } # => 0
166 | # counters.fetch('zoo') # => KeyError: key not found: :zoo
167 | def fetch(key, *extras)
168 | super(convert_key(key), *extras)
169 | end
170 |
171 | # Returns an array of the values at the specified indices:
172 | #
173 | # hash = SymbolizedHash.new
174 | # hash['a'] = 'x'
175 | # hash['b'] = 'y'
176 | # hash.values_at(:a, :b) # => ["x", "y"]
177 | def values_at(*indices)
178 | indices.collect { |key| self[convert_key(key)] }
179 | end
180 |
181 | # Returns a shallow copy of the hash.
182 | #
183 | # hash = SymbolizedHash.new({ a: { b: 'b' } })
184 | # dup = hash.dup
185 | # dup[:a][:c] = 'c'
186 | #
187 | # hash[:a][:c] # => nil
188 | # dup[:a][:c] # => "c"
189 | def dup
190 | self.class.new(self).tap do |new_hash|
191 | set_defaults(new_hash)
192 | end
193 | end
194 |
195 | # This method has the same semantics of +update+, except it does not
196 | # modify the receiver but rather returns a new symbolized hash with
197 | # indifferent access with the result of the merge.
198 | def merge(hash, &block)
199 | self.dup.update(hash, &block)
200 | end
201 |
202 | # Like +merge+ but the other way around: Merges the receiver into the
203 | # argument and returns a new hash with indifferent access as result:
204 | #
205 | # hash = SymbolizedHash.new
206 | # hash[:a] = nil
207 | # hash.reverse_merge('a' => 0, 'b' => 1) # => { :a => nil, :b => 1 }
208 | def reverse_merge(other_hash)
209 | super(self.class.new_from_hash_copying_default(other_hash))
210 | end
211 |
212 | # Same semantics as +reverse_merge+ but modifies the receiver in-place.
213 | def reverse_merge!(other_hash)
214 | replace(reverse_merge( other_hash ))
215 | end
216 |
217 | # Replaces the contents of this hash with other_hash.
218 | #
219 | # h = { "a" => 100, "b" => 200 }
220 | # h.replace({ "c" => 300, "d" => 400 }) # => { :c => 300, :d => 400 }
221 | def replace(other_hash)
222 | super(self.class.new_from_hash_copying_default(other_hash))
223 | end
224 |
225 | # Removes the specified key from the hash.
226 | def delete(key)
227 | super(convert_key(key))
228 | end
229 |
230 | def symbolize_keys!; self end
231 | def deep_symbolize_keys!; self end
232 | def symbolize_keys; dup end
233 | def deep_symbolize_keys; dup end
234 | undef :stringify_keys! if method_defined? :stringify_keys!
235 | undef :deep_stringify_keys! if method_defined? :deep_stringify_keys!
236 | def stringify_keys; to_hash.stringify_keys! end
237 | def deep_stringify_keys; to_hash.deep_stringify_keys! end
238 | def to_options!; self end
239 |
240 | def select(*args, &block)
241 | dup.tap { |hash| hash.select!(*args, &block) }
242 | end
243 |
244 | def reject(*args, &block)
245 | dup.tap { |hash| hash.reject!(*args, &block) }
246 | end
247 |
248 | # Convert to a regular hash with symbol keys.
249 | def to_hash
250 | _new_hash = Hash.new
251 | set_defaults(_new_hash)
252 |
253 | each do |key, value|
254 | _new_hash[key] = convert_value(value, for: :to_hash)
255 | end
256 | _new_hash
257 | end
258 |
259 | protected
260 | def convert_key(key)
261 | key.kind_of?(String) ? key.to_sym : key
262 | end
263 |
264 | def convert_value(value, options = {})
265 | if value.is_a? Hash
266 | if options[:for] == :to_hash
267 | value.to_hash
268 | else
269 | value.nested_under_symbolized_hash
270 | end
271 | elsif value.is_a?(Array)
272 | unless options[:for] == :assignment
273 | value = value.dup
274 | end
275 | value.map! { |e| convert_value(e, options) }
276 | else
277 | value
278 | end
279 | end
280 |
281 | def set_defaults(target)
282 | if default_proc
283 | target.default_proc = default_proc.dup
284 | else
285 | target.default = default
286 | end
287 | end
288 | end
289 | end
290 |
291 | SymbolizedHash = Symbolized::SymbolizedHash
292 |
--------------------------------------------------------------------------------
/lib/symbolized/version.rb:
--------------------------------------------------------------------------------
1 | module Symbolized
2 | VERSION = '0.0.1'
3 | end
--------------------------------------------------------------------------------
/symbolized.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'symbolized/version'
5 |
6 | Gem::Specification.new do |s|
7 | s.name = 'symbolized'
8 | s.version = Symbolized::VERSION
9 | s.licenses = ['MIT']
10 | s.summary = "Symbolized HashWithIndifferentAccess"
11 | s.description = "Hash with indifferent access, with keys stored internally as symbols."
12 | s.authors = ["Tamer Shlash"]
13 | s.email = 'mr.tamershlash@gmail.com'
14 | s.files = Dir["lib/**/*", "LICENSE", "README.md"]
15 | s.require_path = 'lib'
16 | s.homepage = 'https://github.com/TamerShlash/symbolized'
17 | end
18 |
--------------------------------------------------------------------------------
/test/core_ext/hash_ext_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helpers'
2 | require 'symbolized/core_ext/object/deep_dup'
3 | require 'symbolized/core_ext/hash/symbolized_hash'
4 |
5 | class HashExtTest < Symbolized::TestCase
6 | class IndifferentHash < Symbolized::SymbolizedHash
7 | end
8 |
9 | class SubclassingArray < Array
10 | end
11 |
12 | class SubclassingHash < Hash
13 | end
14 |
15 | class NonIndifferentHash < Hash
16 | def nested_under_symbolized_hash
17 | self
18 | end
19 | end
20 |
21 | class HashByConversion
22 | def initialize(hash)
23 | @hash = hash
24 | end
25 |
26 | def to_hash
27 | @hash
28 | end
29 | end
30 |
31 | def setup
32 | @strings = { 'a' => 1, 'b' => 2 }
33 | @nested_strings = { 'a' => { 'b' => { 'c' => 3 } } }
34 | @symbols = { :a => 1, :b => 2 }
35 | @nested_symbols = { :a => { :b => { :c => 3 } } }
36 | @mixed = { :a => 1, 'b' => 2 }
37 | @nested_mixed = { 'a' => { :b => { 'c' => 3 } } }
38 | @fixnums = { 0 => 1, 1 => 2 }
39 | @nested_fixnums = { 0 => { 1 => { 2 => 3} } }
40 | @illegal_symbols = { [] => 3 }
41 | @nested_illegal_symbols = { [] => { [] => 3} }
42 | @upcase_strings = { 'A' => 1, 'B' => 2 }
43 | @nested_upcase_strings = { 'A' => { 'B' => { 'C' => 3 } } }
44 | @string_array_of_hashes = { 'a' => [ { 'b' => 2 }, { 'c' => 3 }, 4 ] }
45 | @symbol_array_of_hashes = { :a => [ { :b => 2 }, { :c => 3 }, 4 ] }
46 | @mixed_array_of_hashes = { :a => [ { :b => 2 }, { 'c' => 3 }, 4 ] }
47 | @upcase_array_of_hashes = { 'A' => [ { 'B' => 2 }, { 'C' => 3 }, 4 ] }
48 | end
49 |
50 | def test_methods
51 | h = {}
52 | assert_respond_to h, :transform_keys
53 | assert_respond_to h, :transform_keys!
54 | assert_respond_to h, :deep_transform_keys
55 | assert_respond_to h, :deep_transform_keys!
56 | assert_respond_to h, :symbolize_keys
57 | assert_respond_to h, :symbolize_keys!
58 | assert_respond_to h, :deep_symbolize_keys
59 | assert_respond_to h, :deep_symbolize_keys!
60 | assert_respond_to h, :stringify_keys
61 | assert_respond_to h, :stringify_keys!
62 | assert_respond_to h, :deep_stringify_keys
63 | assert_respond_to h, :deep_stringify_keys!
64 | assert_respond_to h, :to_options
65 | assert_respond_to h, :to_options!
66 | end
67 |
68 | def test_stringify_keys_for_symbolized_hash
69 | assert_instance_of Hash, @strings.to_symbolized_hash.stringify_keys
70 | assert_equal @strings, @strings.to_symbolized_hash.stringify_keys
71 | assert_equal @strings, @symbols.to_symbolized_hash.stringify_keys
72 | assert_equal @strings, @mixed.to_symbolized_hash.stringify_keys
73 | end
74 |
75 | def test_deep_stringify_keys_for_symbolized_hash
76 | assert_instance_of Hash, @nested_symbols.to_symbolized_hash.deep_stringify_keys
77 | assert_equal @nested_strings, @nested_strings.to_symbolized_hash.deep_stringify_keys
78 | assert_equal @nested_strings, @nested_symbols.to_symbolized_hash.deep_stringify_keys
79 | assert_equal @nested_strings, @nested_mixed.to_symbolized_hash.deep_stringify_keys
80 | end
81 |
82 | def test_stringify_keys_bang_for_symbolized_hash
83 | assert_raise(NoMethodError) { @strings.to_symbolized_hash.dup.stringify_keys! }
84 | assert_raise(NoMethodError) { @symbols.to_symbolized_hash.dup.stringify_keys! }
85 | assert_raise(NoMethodError) { @mixed.to_symbolized_hash.dup.stringify_keys! }
86 | end
87 |
88 | def test_deep_stringify_keys_bang_for_symbolized_hash
89 | assert_raise(NoMethodError) { @nested_strings.to_symbolized_hash.deep_dup.deep_stringify_keys! }
90 | assert_raise(NoMethodError) { @nested_symbols.to_symbolized_hash.deep_dup.deep_stringify_keys! }
91 | assert_raise(NoMethodError) { @nested_mixed.to_symbolized_hash.deep_dup.deep_stringify_keys! }
92 | end
93 |
94 | def test_symbolize_keys_for_symbolized_hash
95 | assert_instance_of Symbolized::SymbolizedHash, @symbols.to_symbolized_hash.symbolize_keys
96 | assert_equal @symbols, @strings.to_symbolized_hash.symbolize_keys
97 | assert_equal @symbols, @symbols.to_symbolized_hash.symbolize_keys
98 | assert_equal @symbols, @mixed.to_symbolized_hash.symbolize_keys
99 | end
100 |
101 | def test_deep_stringify_keys_for_symbolized_hash
102 | assert_instance_of Symbolized::SymbolizedHash, @nested_symbols.to_symbolized_hash.deep_symbolize_keys
103 | assert_equal @nested_symbols, @nested_strings.to_symbolized_hash.deep_symbolize_keys
104 | assert_equal @nested_symbols, @nested_symbols.to_symbolized_hash.deep_symbolize_keys
105 | assert_equal @nested_symbols, @nested_mixed.to_symbolized_hash.deep_symbolize_keys
106 | end
107 |
108 | def test_symbolize_keys_bang_for_symbolized_hash
109 | assert_instance_of Symbolized::SymbolizedHash, @symbols.to_symbolized_hash.dup.symbolize_keys!
110 | assert_equal @symbols, @strings.to_symbolized_hash.dup.symbolize_keys!
111 | assert_equal @symbols, @symbols.to_symbolized_hash.dup.symbolize_keys!
112 | assert_equal @symbols, @mixed.to_symbolized_hash.dup.symbolize_keys!
113 | end
114 |
115 | def test_deep_symbolize_keys_bang_for_symbolized_hash
116 | assert_instance_of Symbolized::SymbolizedHash, @nested_symbols.to_symbolized_hash.dup.deep_symbolize_keys!
117 | assert_equal @nested_symbols, @nested_strings.to_symbolized_hash.deep_dup.deep_symbolize_keys!
118 | assert_equal @nested_symbols, @nested_symbols.to_symbolized_hash.deep_dup.deep_symbolize_keys!
119 | assert_equal @nested_symbols, @nested_mixed.to_symbolized_hash.deep_dup.deep_symbolize_keys!
120 | end
121 |
122 | def test_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_symbolized_hash
123 | assert_equal @illegal_symbols, @illegal_symbols.to_symbolized_hash.symbolize_keys
124 | assert_equal @illegal_symbols, @illegal_symbols.to_symbolized_hash.dup.symbolize_keys!
125 | end
126 |
127 | def test_deep_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_symbolized_hash
128 | assert_equal @nested_illegal_symbols, @nested_illegal_symbols.to_symbolized_hash.deep_symbolize_keys
129 | assert_equal @nested_illegal_symbols, @nested_illegal_symbols.to_symbolized_hash.deep_dup.deep_symbolize_keys!
130 | end
131 |
132 | def test_symbolize_keys_preserves_fixnum_keys_for_symbolized_hash
133 | assert_equal @fixnums, @fixnums.to_symbolized_hash.symbolize_keys
134 | assert_equal @fixnums, @fixnums.to_symbolized_hash.dup.symbolize_keys!
135 | end
136 |
137 | def test_deep_symbolize_keys_preserves_fixnum_keys_for_symbolized_hash
138 | assert_equal @nested_fixnums, @nested_fixnums.to_symbolized_hash.deep_symbolize_keys
139 | assert_equal @nested_fixnums, @nested_fixnums.to_symbolized_hash.deep_dup.deep_symbolize_keys!
140 | end
141 |
142 | def test_nested_under_symbolized_hash
143 | foo = { "foo" => SubclassingHash.new.tap { |h| h["bar"] = "baz" } }.to_symbolized_hash
144 | assert_kind_of Symbolized::SymbolizedHash, foo["foo"]
145 |
146 | foo = { "foo" => NonIndifferentHash.new.tap { |h| h["bar"] = "baz" } }.to_symbolized_hash
147 | assert_kind_of NonIndifferentHash, foo["foo"]
148 |
149 | foo = { "foo" => IndifferentHash.new.tap { |h| h["bar"] = "baz" } }.to_symbolized_hash
150 | assert_kind_of IndifferentHash, foo["foo"]
151 | end
152 |
153 | def test_indifferent_assorted
154 | @strings = @strings.to_symbolized_hash
155 | @symbols = @symbols.to_symbolized_hash
156 | @mixed = @mixed.to_symbolized_hash
157 |
158 | assert_equal :a, @strings.__send__(:convert_key, 'a')
159 |
160 | assert_equal 1, @strings.fetch(:a)
161 | assert_equal 1, @strings.fetch('a'.to_sym)
162 | assert_equal 1, @strings.fetch('a')
163 |
164 | hashes = { :@strings => @strings, :@symbols => @symbols, :@mixed => @mixed }
165 | method_map = { :'[]' => 1, :fetch => 1, :values_at => [1],
166 | :has_key? => true, :include? => true, :key? => true,
167 | :member? => true }
168 |
169 | hashes.each do |name, hash|
170 | method_map.sort_by { |m| m.to_s }.each do |meth, expected|
171 | assert_equal(expected, hash.__send__(meth, 'a'),
172 | "Calling #{name}.#{meth} 'a'")
173 | assert_equal(expected, hash.__send__(meth, :a),
174 | "Calling #{name}.#{meth} :a")
175 | end
176 | end
177 |
178 | assert_equal [1, 2], @strings.values_at('a', 'b')
179 | assert_equal [1, 2], @strings.values_at(:a, :b)
180 | assert_equal [1, 2], @symbols.values_at('a', 'b')
181 | assert_equal [1, 2], @symbols.values_at(:a, :b)
182 | assert_equal [1, 2], @mixed.values_at('a', 'b')
183 | assert_equal [1, 2], @mixed.values_at(:a, :b)
184 | end
185 |
186 | def test_indifferent_reading
187 | hash = SymbolizedHash.new
188 | hash["a"] = 1
189 | hash["b"] = true
190 | hash["c"] = false
191 | hash["d"] = nil
192 |
193 | assert_equal 1, hash[:a]
194 | assert_equal true, hash[:b]
195 | assert_equal false, hash[:c]
196 | assert_equal nil, hash[:d]
197 | assert_equal nil, hash[:e]
198 | end
199 |
200 | def test_indifferent_reading_with_nonnil_default
201 | hash = SymbolizedHash.new(1)
202 | hash["a"] = 1
203 | hash["b"] = true
204 | hash["c"] = false
205 | hash["d"] = nil
206 |
207 | assert_equal 1, hash[:a]
208 | assert_equal true, hash[:b]
209 | assert_equal false, hash[:c]
210 | assert_equal nil, hash[:d]
211 | assert_equal 1, hash[:e]
212 | end
213 |
214 | def test_indifferent_writing
215 | hash = SymbolizedHash.new
216 | hash[:a] = 1
217 | hash['b'] = 2
218 | hash[3] = 3
219 |
220 | assert_equal hash['a'], 1
221 | assert_equal hash['b'], 2
222 | assert_equal hash[:a], 1
223 | assert_equal hash[:b], 2
224 | assert_equal hash[3], 3
225 | end
226 |
227 | def test_indifferent_update
228 | hash = SymbolizedHash.new
229 | hash[:a] = 'a'
230 | hash['b'] = 'b'
231 |
232 | updated_with_strings = hash.update(@strings)
233 | updated_with_symbols = hash.update(@symbols)
234 | updated_with_mixed = hash.update(@mixed)
235 |
236 | assert_equal updated_with_strings[:a], 1
237 | assert_equal updated_with_strings['a'], 1
238 | assert_equal updated_with_strings['b'], 2
239 |
240 | assert_equal updated_with_symbols[:a], 1
241 | assert_equal updated_with_symbols['b'], 2
242 | assert_equal updated_with_symbols[:b], 2
243 |
244 | assert_equal updated_with_mixed[:a], 1
245 | assert_equal updated_with_mixed['b'], 2
246 |
247 | assert [updated_with_strings, updated_with_symbols, updated_with_mixed].all? { |h| h.keys.size == 2 }
248 | end
249 |
250 | def test_update_with_to_hash_conversion
251 | hash = SymbolizedHash.new
252 | hash.update HashByConversion.new({ :a => 1 })
253 | assert_equal hash['a'], 1
254 | end
255 |
256 | def test_indifferent_merging
257 | hash = SymbolizedHash.new
258 | hash[:a] = 'failure'
259 | hash['b'] = 'failure'
260 |
261 | other = { 'a' => 1, :b => 2 }
262 |
263 | merged = hash.merge(other)
264 |
265 | assert_equal SymbolizedHash, merged.class
266 | assert_equal 1, merged[:a]
267 | assert_equal 2, merged['b']
268 |
269 | hash.update(other)
270 |
271 | assert_equal 1, hash[:a]
272 | assert_equal 2, hash['b']
273 | end
274 |
275 | def test_merge_with_to_hash_conversion
276 | hash = SymbolizedHash.new
277 | merged = hash.merge HashByConversion.new({ :a => 1 })
278 | assert_equal merged['a'], 1
279 | end
280 |
281 | def test_indifferent_replace
282 | hash = SymbolizedHash.new
283 | hash[:a] = 42
284 |
285 | replaced = hash.replace(b: 12)
286 |
287 | assert hash.key?('b')
288 | assert !hash.key?(:a)
289 | assert_equal 12, hash[:b]
290 | assert_same hash, replaced
291 | end
292 |
293 | def test_replace_with_to_hash_conversion
294 | hash = SymbolizedHash.new
295 | hash[:a] = 42
296 |
297 | replaced = hash.replace(HashByConversion.new(b: 12))
298 |
299 | assert hash.key?('b')
300 | assert !hash.key?(:a)
301 | assert_equal 12, hash[:b]
302 | assert_same hash, replaced
303 | end
304 |
305 | def test_indifferent_merging_with_block
306 | hash = SymbolizedHash.new
307 | hash[:a] = 1
308 | hash['b'] = 3
309 |
310 | other = { 'a' => 4, :b => 2, 'c' => 10 }
311 |
312 | merged = hash.merge(other) { |key, old, new| old > new ? old : new }
313 |
314 | assert_equal SymbolizedHash, merged.class
315 | assert_equal 4, merged[:a]
316 | assert_equal 3, merged['b']
317 | assert_equal 10, merged[:c]
318 |
319 | other_indifferent = SymbolizedHash.new('a' => 9, :b => 2)
320 |
321 | merged = hash.merge(other_indifferent) { |key, old, new| old + new }
322 |
323 | assert_equal SymbolizedHash, merged.class
324 | assert_equal 10, merged[:a]
325 | assert_equal 5, merged[:b]
326 | end
327 |
328 | def test_indifferent_reverse_merging
329 | hash = SymbolizedHash.new key: :old_value
330 | hash.reverse_merge! key: :new_value
331 | assert_equal :old_value, hash[:key]
332 |
333 | hash = SymbolizedHash.new('some' => 'value', 'other' => 'value')
334 | hash.reverse_merge!(:some => 'noclobber', :another => 'clobber')
335 | assert_equal 'value', hash[:some]
336 | assert_equal 'clobber', hash[:another]
337 | end
338 |
339 | def test_indifferent_deleting
340 | get_hash = proc{ { :a => 'foo' }.to_symbolized_hash }
341 | hash = get_hash.call
342 | assert_equal hash.delete(:a), 'foo'
343 | assert_equal hash.delete(:a), nil
344 | hash = get_hash.call
345 | assert_equal hash.delete('a'), 'foo'
346 | assert_equal hash.delete('a'), nil
347 | end
348 |
349 | def test_indifferent_select
350 | hash = Symbolized::SymbolizedHash.new(@strings).select {|k,v| v == 1}
351 |
352 | assert_equal({ :a => 1 }, hash)
353 | assert_instance_of Symbolized::SymbolizedHash, hash
354 | end
355 |
356 | def test_indifferent_select_returns_a_hash_when_unchanged
357 | hash = Symbolized::SymbolizedHash.new(@strings).select {|k,v| true}
358 |
359 | assert_instance_of Symbolized::SymbolizedHash, hash
360 | end
361 |
362 | def test_indifferent_select_bang
363 | indifferent_symbols = Symbolized::SymbolizedHash.new(@symbols)
364 | indifferent_symbols.select! {|k,v| v == 1}
365 |
366 | assert_equal({ :a => 1 }, indifferent_symbols)
367 | assert_instance_of Symbolized::SymbolizedHash, indifferent_symbols
368 | end
369 |
370 | def test_indifferent_reject
371 | hash = Symbolized::SymbolizedHash.new(@symbols).reject {|k,v| v != 1}
372 |
373 | assert_equal({ :a => 1 }, hash)
374 | assert_instance_of Symbolized::SymbolizedHash, hash
375 | end
376 |
377 | def test_indifferent_reject_bang
378 | indifferent_symbols = Symbolized::SymbolizedHash.new(@symbols)
379 | indifferent_symbols.reject! {|k,v| v != 1}
380 |
381 | assert_equal({ :a => 1 }, indifferent_symbols)
382 | assert_instance_of Symbolized::SymbolizedHash, indifferent_symbols
383 | end
384 |
385 | def test_indifferent_to_hash
386 | # Should convert to a Hash with Symbol keys.
387 | assert_equal @symbols, @mixed.to_symbolized_hash.to_hash
388 |
389 | # Should preserve the default value.
390 | mixed_with_default = @mixed.dup
391 | mixed_with_default.default = '1234'
392 | roundtrip = mixed_with_default.to_symbolized_hash.to_hash
393 | assert_equal @symbols, roundtrip
394 | assert_equal '1234', roundtrip.default
395 |
396 | # Ensure nested hashes are not HashWithIndiffereneAccess
397 | new_to_hash = @nested_mixed.to_symbolized_hash.to_hash
398 | assert_not new_to_hash.instance_of?(SymbolizedHash)
399 | assert_not new_to_hash[:a].instance_of?(SymbolizedHash)
400 | assert_not new_to_hash[:a][:b].instance_of?(SymbolizedHash)
401 | end
402 |
403 | def test_lookup_returns_the_same_object_that_is_stored_in_symbolized_hash
404 | hash = SymbolizedHash.new {|h, k| h[k] = []}
405 | hash[:a] << 1
406 |
407 | assert_equal [1], hash[:a]
408 | end
409 |
410 | def test_symbolized_hash_has_no_side_effects_on_existing_hash
411 | hash = {content: [{:foo => :bar, 'bar' => 'baz'}]}
412 | hash.to_symbolized_hash
413 |
414 | assert_equal [:foo, "bar"], hash[:content].first.keys
415 | end
416 |
417 | def test_indifferent_hash_with_array_of_hashes
418 | hash = { "urls" => { "url" => [ { "address" => "1" }, { "address" => "2" } ] }}.to_symbolized_hash
419 | assert_equal "1", hash[:urls][:url].first[:address]
420 |
421 | hash = hash.to_hash
422 | assert_not hash.instance_of?(SymbolizedHash)
423 | assert_not hash[:urls].instance_of?(SymbolizedHash)
424 | assert_not hash[:urls][:url].first.instance_of?(SymbolizedHash)
425 | end
426 |
427 | def test_should_preserve_array_subclass_when_value_is_array
428 | array = SubclassingArray.new
429 | array << { "address" => "1" }
430 | hash = { "urls" => { "url" => array }}.to_symbolized_hash
431 | assert_equal SubclassingArray, hash[:urls][:url].class
432 | end
433 |
434 | def test_should_preserve_array_class_when_hash_value_is_frozen_array
435 | array = SubclassingArray.new
436 | array << { "address" => "1" }
437 | hash = { "urls" => { "url" => array.freeze }}.to_symbolized_hash
438 | assert_equal SubclassingArray, hash[:urls][:url].class
439 | end
440 |
441 | def test_stringify_and_symbolize_keys_on_indifferent_preserves_hash
442 | h = SymbolizedHash.new
443 | h[:first] = 1
444 | h = h.stringify_keys
445 | assert_equal 1, h['first']
446 | h = SymbolizedHash.new
447 | h['first'] = 1
448 | h = h.symbolize_keys
449 | assert_equal 1, h[:first]
450 | end
451 |
452 | def test_deep_stringify_and_deep_symbolize_keys_on_indifferent_preserves_hash
453 | h = SymbolizedHash.new
454 | h[:first] = 1
455 | h = h.deep_stringify_keys
456 | assert_equal 1, h['first']
457 | h = SymbolizedHash.new
458 | h['first'] = 1
459 | h = h.deep_symbolize_keys
460 | assert_equal 1, h[:first]
461 | end
462 |
463 | def test_to_options_on_indifferent_preserves_hash
464 | h = SymbolizedHash.new
465 | h['first'] = 1
466 | h.to_options!
467 | assert_equal 1, h['first']
468 | end
469 |
470 | def test_to_options_on_indifferent_preserves_works_as_hash_with_dup
471 | h = SymbolizedHash.new({ a: { b: 'b' } })
472 | dup = h.dup
473 |
474 | dup[:a][:c] = 'c'
475 | assert_equal 'c', h[:a][:c]
476 | end
477 |
478 | def test_indifferent_sub_hashes
479 | h = {'user' => {'id' => 5}}.to_symbolized_hash
480 | ['user', :user].each {|user| [:id, 'id'].each {|id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5"}}
481 |
482 | h = {:user => {:id => 5}}.to_symbolized_hash
483 | ['user', :user].each {|user| [:id, 'id'].each {|id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5"}}
484 | end
485 |
486 | def test_indifferent_duplication
487 | # Should preserve default value
488 | h = SymbolizedHash.new
489 | h.default = '1234'
490 | assert_equal h.default, h.dup.default
491 |
492 | # Should preserve class for subclasses
493 | h = IndifferentHash.new
494 | assert_equal h.class, h.dup.class
495 | end
496 |
497 | def test_assert_valid_keys
498 | assert_nothing_raised do
499 | { :failure => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ])
500 | { :failure => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny)
501 | end
502 | # not all valid keys are required to be present
503 | assert_nothing_raised do
504 | { :failure => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny, :sunny ])
505 | { :failure => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny, :sunny)
506 | end
507 |
508 | exception = assert_raise ArgumentError do
509 | { :failore => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ])
510 | end
511 | assert_equal "Unknown key: :failore. Valid keys are: :failure, :funny", exception.message
512 |
513 | exception = assert_raise ArgumentError do
514 | { :failore => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny)
515 | end
516 | assert_equal "Unknown key: :failore. Valid keys are: :failure, :funny", exception.message
517 |
518 | exception = assert_raise ArgumentError do
519 | { :failore => "stuff", :funny => "business" }.assert_valid_keys([ :failure ])
520 | end
521 | assert_equal "Unknown key: :failore. Valid keys are: :failure", exception.message
522 |
523 | exception = assert_raise ArgumentError do
524 | { :failore => "stuff", :funny => "business" }.assert_valid_keys(:failure)
525 | end
526 | assert_equal "Unknown key: :failore. Valid keys are: :failure", exception.message
527 | end
528 |
529 | def test_assorted_keys_not_stringified
530 | original = {Object.new => 2, 1 => 2, [] => true}
531 | indiff = original.to_symbolized_hash
532 | assert(!indiff.keys.any? {|k| k.kind_of? String}, "A key was converted to a string!")
533 | end
534 |
535 | def test_store_on_symbolized_hash
536 | hash = SymbolizedHash.new
537 | hash.store(:test1, 1)
538 | hash.store('test1', 11)
539 | hash[:test2] = 2
540 | hash['test2'] = 22
541 | expected = { :test1 => 11, :test2 => 22 }
542 | assert_equal expected, hash
543 | end
544 |
545 | def test_constructor_on_symbolized_hash
546 | hash = SymbolizedHash[:foo, 1]
547 | assert_equal 1, hash[:foo]
548 | assert_equal 1, hash['foo']
549 | hash[:foo] = 3
550 | assert_equal 3, hash[:foo]
551 | assert_equal 3, hash['foo']
552 | end
553 |
554 | def test_reverse_merge
555 | defaults = { :a => "x", :b => "y", :c => 10 }.freeze
556 | options = { :a => 1, :b => 2 }
557 | expected = { :a => 1, :b => 2, :c => 10 }
558 |
559 | # Should merge defaults into options, creating a new hash.
560 | assert_equal expected, options.reverse_merge(defaults)
561 | assert_not_equal expected, options
562 |
563 | # Should merge! defaults into options, replacing options.
564 | merged = options.dup
565 | assert_equal expected, merged.reverse_merge!(defaults)
566 | assert_equal expected, merged
567 |
568 | # Should be an alias for reverse_merge!
569 | merged = options.dup
570 | assert_equal expected, merged.reverse_update(defaults)
571 | assert_equal expected, merged
572 | end
573 |
574 | def test_new_with_to_hash_conversion
575 | hash = SymbolizedHash.new(HashByConversion.new(a: 1))
576 | assert hash.key?('a')
577 | assert_equal 1, hash[:a]
578 | end
579 |
580 | def test_dup_with_default_proc
581 | hash = SymbolizedHash.new
582 | hash.default_proc = proc { |h, v| raise "walrus" }
583 | assert_nothing_raised { hash.dup }
584 | end
585 |
586 | def test_dup_with_default_proc_sets_proc
587 | hash = SymbolizedHash.new
588 | hash.default_proc = proc { |h, k| k + 1 }
589 | new_hash = hash.dup
590 |
591 | assert_equal 3, new_hash[2]
592 |
593 | new_hash.default = 2
594 | assert_equal 2, new_hash[:non_existant]
595 | end
596 |
597 | def test_to_hash_with_raising_default_proc
598 | hash = SymbolizedHash.new
599 | hash.default_proc = proc { |h, k| raise "walrus" }
600 |
601 | assert_nothing_raised { hash.to_hash }
602 | end
603 |
604 | def test_new_from_hash_copying_default_should_not_raise_when_default_proc_does
605 | hash = Hash.new
606 | hash.default_proc = proc { |h, k| raise "walrus" }
607 |
608 | assert_nothing_raised { SymbolizedHash.new_from_hash_copying_default(hash) }
609 | end
610 | end
611 |
--------------------------------------------------------------------------------
/test/test_helpers.rb:
--------------------------------------------------------------------------------
1 | gem 'minitest'
2 | require 'minitest'
3 | require 'minitest/autorun'
4 |
5 | module Symbolized
6 | class TestCase < ::Minitest::Test
7 | # test/unit backwards compatibility methods
8 | alias :assert_raise :assert_raises
9 | alias :assert_not_empty :refute_empty
10 | alias :assert_not_equal :refute_equal
11 | alias :assert_not_in_delta :refute_in_delta
12 | alias :assert_not_in_epsilon :refute_in_epsilon
13 | alias :assert_not_includes :refute_includes
14 | alias :assert_not_instance_of :refute_instance_of
15 | alias :assert_not_kind_of :refute_kind_of
16 | alias :assert_no_match :refute_match
17 | alias :assert_not_nil :refute_nil
18 | alias :assert_not_operator :refute_operator
19 | alias :assert_not_predicate :refute_predicate
20 | alias :assert_not_respond_to :refute_respond_to
21 | alias :assert_not_same :refute_same
22 |
23 | # Fails if the block raises an exception.
24 | #
25 | # assert_nothing_raised do
26 | # ...
27 | # end
28 | def assert_nothing_raised(*args)
29 | yield
30 | end
31 |
32 | # Assert that an expression is not truthy. Passes if object is
33 | # +nil+ or +false+. "Truthy" means "considered true in a conditional"
34 | # like if foo.
35 | #
36 | # assert_not nil # => true
37 | # assert_not false # => true
38 | # assert_not 'foo' # => Expected "foo" to be nil or false
39 | #
40 | # An error message can be specified.
41 | #
42 | # assert_not foo, 'foo should be false'
43 | def assert_not(object, message = nil)
44 | message ||= "Expected #{mu_pp(object)} to be nil or false"
45 | assert !object, message
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------