symbolized 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +13 -5
- data/LICENSE +22 -0
- data/README.md +85 -0
- data/lib/symbolized.rb +2 -0
- data/lib/symbolized/core_ext/hash/keys.rb +166 -0
- data/lib/symbolized/core_ext/hash/reverse_merge.rb +22 -0
- data/lib/symbolized/core_ext/hash/symbolized_hash.rb +21 -0
- data/lib/symbolized/core_ext/object/deep_dup.rb +48 -0
- data/lib/symbolized/core_ext/object/duplicable.rb +105 -0
- data/lib/symbolized/symbolized_hash.rb +291 -0
- data/lib/symbolized/version.rb +3 -0
- metadata +14 -5
checksums.yaml
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ODFiMWZjNjFhZjdjMzk1MDYyMmMzNDIxMzI4YmE4MTE2YTY2OWQzMw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NmUwZGYzMDk0ZGI3ZTJiMjcxZWY5MmQxNjFhZWY3ZDJhOTIzY2U1NA==
|
5
7
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YjJhYTMyZDZkM2I2OWQ3N2UzNzAxY2RjZmE1MThiM2I5MGJjM2QwNTdjZDAx
|
10
|
+
ODBmMzZhNWNiNGVlNTEwMDFiOTY1MDdiZmRhYjUyNDJjNTIwYzAzMjM4N2M4
|
11
|
+
M2M5ZWUwNWM5OTY0OGE5ZjFiYWU1ODRiZDBhYTMxMTQ3YmIwYWU=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MDYyOGQ1N2ExNjEyNjQxNmI1NzY4NDVmOGUyMTY3NTVmNTgyMDVjNThkM2Nm
|
14
|
+
YWMyMzhiNmU2ZTIxMjU1OGE0MDFkYjQ3NDAzMDRmM2NlMGNhMDg4NTU2NmVh
|
15
|
+
MjA5ZWFjOTJiODFlM2MzMDk4NjllOGJhOTNkZmQ2NTg0NDlkMTA=
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
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
|
+
|
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
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).
|
data/lib/symbolized.rb
CHANGED
@@ -0,0 +1,166 @@
|
|
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 <tt>*valid_keys</tt>, 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
|
@@ -0,0 +1,22 @@
|
|
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
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'symbolized'
|
2
|
+
|
3
|
+
class Hash
|
4
|
+
# Returns a <tt>Symbolized::SymbolizedHash</tt> 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 <tt>Symbolized::SymbolizedHash</tt> 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
|
@@ -0,0 +1,48 @@
|
|
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
|
@@ -0,0 +1,105 @@
|
|
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
|
@@ -0,0 +1,291 @@
|
|
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 <tt>:foo</tt> and <tt>"foo"</tt> 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 <tt>[]=</tt>, <tt>merge</tt>, 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 <tt>Hash#to_symbolized_hash</tt>:
|
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 <tt>Array#extract_options!</tt> 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 <tt>SymbolizedHash</tt> 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 <tt>Hash#fetch</tt> 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
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: symbolized
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tamer Shlash
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-07-
|
11
|
+
date: 2015-07-19 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Hash with indifferent access, with keys stored internally as symbols.
|
14
14
|
email: mr.tamershlash@gmail.com
|
@@ -16,7 +16,16 @@ executables: []
|
|
16
16
|
extensions: []
|
17
17
|
extra_rdoc_files: []
|
18
18
|
files:
|
19
|
+
- LICENSE
|
20
|
+
- README.md
|
19
21
|
- lib/symbolized.rb
|
22
|
+
- lib/symbolized/core_ext/hash/keys.rb
|
23
|
+
- lib/symbolized/core_ext/hash/reverse_merge.rb
|
24
|
+
- lib/symbolized/core_ext/hash/symbolized_hash.rb
|
25
|
+
- lib/symbolized/core_ext/object/deep_dup.rb
|
26
|
+
- lib/symbolized/core_ext/object/duplicable.rb
|
27
|
+
- lib/symbolized/symbolized_hash.rb
|
28
|
+
- lib/symbolized/version.rb
|
20
29
|
homepage: https://github.com/TamerShlash/symbolized
|
21
30
|
licenses:
|
22
31
|
- MIT
|
@@ -27,17 +36,17 @@ require_paths:
|
|
27
36
|
- lib
|
28
37
|
required_ruby_version: !ruby/object:Gem::Requirement
|
29
38
|
requirements:
|
30
|
-
- -
|
39
|
+
- - ! '>='
|
31
40
|
- !ruby/object:Gem::Version
|
32
41
|
version: '0'
|
33
42
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
34
43
|
requirements:
|
35
|
-
- -
|
44
|
+
- - ! '>='
|
36
45
|
- !ruby/object:Gem::Version
|
37
46
|
version: '0'
|
38
47
|
requirements: []
|
39
48
|
rubyforge_project:
|
40
|
-
rubygems_version: 2.
|
49
|
+
rubygems_version: 2.2.2
|
41
50
|
signing_key:
|
42
51
|
specification_version: 4
|
43
52
|
summary: Symbolized HashWithIndifferentAccess
|