hashie 5.2.0 → 6.0.0
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 +4 -4
- data/CHANGELOG.md +19 -0
- data/CONTRIBUTING.md +2 -0
- data/README.md +60 -10
- data/Rakefile +15 -0
- data/UPGRADING.md +100 -6
- data/lib/hashie/clash.rb +3 -2
- data/lib/hashie/dash.rb +25 -25
- data/lib/hashie/extensions/block_access.rb +5 -5
- data/lib/hashie/extensions/dash/coercion.rb +4 -4
- data/lib/hashie/extensions/dash/indifferent_access.rb +5 -5
- data/lib/hashie/extensions/dash/lazy.rb +10 -10
- data/lib/hashie/extensions/dash/predefined_values.rb +12 -16
- data/lib/hashie/extensions/dash/property_translation.rb +22 -14
- data/lib/hashie/extensions/deep_find.rb +4 -4
- data/lib/hashie/extensions/deep_grep.rb +15 -15
- data/lib/hashie/extensions/deep_locate.rb +3 -3
- data/lib/hashie/extensions/deep_merge.rb +16 -14
- data/lib/hashie/extensions/deep_merge_initializer.rb +3 -3
- data/lib/hashie/extensions/indifferent_access.rb +6 -6
- data/lib/hashie/extensions/mash/define_accessors.rb +3 -1
- data/lib/hashie/extensions/mash/keep_original_keys.rb +2 -2
- data/lib/hashie/extensions/mash/permissive_respond_to.rb +1 -1
- data/lib/hashie/extensions/mash/required_keys.rb +2 -2
- data/lib/hashie/extensions/mash/strict_key_access.rb +2 -2
- data/lib/hashie/extensions/method_access.rb +15 -15
- data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +1 -1
- data/lib/hashie/extensions/parsers/yaml_parser.rb +2 -2
- data/lib/hashie/extensions/persistable.rb +4 -4
- data/lib/hashie/extensions/ruby_version.rb +2 -2
- data/lib/hashie/extensions/stringify_keys.rb +4 -3
- data/lib/hashie/extensions/symbolize_keys.rb +4 -3
- data/lib/hashie/hash.rb +5 -5
- data/lib/hashie/mash.rb +20 -16
- data/lib/hashie/rash.rb +2 -2
- data/lib/hashie/utils.rb +3 -2
- data/lib/hashie/version.rb +1 -1
- metadata +3 -3
|
@@ -132,7 +132,7 @@ module Hashie
|
|
|
132
132
|
# hash.respond_to?(:name?) # => true
|
|
133
133
|
# hash.respond_to?(:missing?) # => false
|
|
134
134
|
def respond_to_missing?(name, include_private = false)
|
|
135
|
-
if
|
|
135
|
+
if _query_method?(name) && _indifferent_key?(_key_from_query_method(name))
|
|
136
136
|
true
|
|
137
137
|
else
|
|
138
138
|
super
|
|
@@ -146,9 +146,9 @@ module Hashie
|
|
|
146
146
|
def method_missing(name, *args)
|
|
147
147
|
return super unless args.empty?
|
|
148
148
|
|
|
149
|
-
if
|
|
150
|
-
key =
|
|
151
|
-
if
|
|
149
|
+
if _query_method?(name)
|
|
150
|
+
key = _key_from_query_method(name)
|
|
151
|
+
if _indifferent_key?(key)
|
|
152
152
|
!!(self[key] || self[key.to_sym])
|
|
153
153
|
else
|
|
154
154
|
super
|
|
@@ -160,16 +160,16 @@ module Hashie
|
|
|
160
160
|
|
|
161
161
|
private
|
|
162
162
|
|
|
163
|
-
def
|
|
163
|
+
def _indifferent_key?(name)
|
|
164
164
|
name = name.to_s
|
|
165
165
|
key?(name) || key?(name.to_sym)
|
|
166
166
|
end
|
|
167
167
|
|
|
168
|
-
def
|
|
168
|
+
def _key_from_query_method(query_method)
|
|
169
169
|
query_method.to_s[0..-2]
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
-
def
|
|
172
|
+
def _query_method?(name)
|
|
173
173
|
name.to_s.end_with?('?')
|
|
174
174
|
end
|
|
175
175
|
end
|
|
@@ -204,13 +204,13 @@ module Hashie
|
|
|
204
204
|
# hash.size # => 'large'
|
|
205
205
|
# hash.__size # => 1
|
|
206
206
|
module RedefineMethod
|
|
207
|
-
|
|
207
|
+
private
|
|
208
208
|
|
|
209
|
-
def
|
|
209
|
+
def _method?(name)
|
|
210
210
|
methods.map(&:to_s).include?(name)
|
|
211
211
|
end
|
|
212
212
|
|
|
213
|
-
def
|
|
213
|
+
def _redefine_method(method_name)
|
|
214
214
|
eigenclass = class << self; self; end
|
|
215
215
|
eigenclass.__send__(:alias_method, "__#{method_name}", method_name)
|
|
216
216
|
eigenclass.__send__(:define_method, method_name, -> { self[method_name] })
|
|
@@ -262,7 +262,7 @@ module Hashie
|
|
|
262
262
|
def method_missing(name, *args)
|
|
263
263
|
if args.size == 1 && name.to_s =~ /(.*)=$/
|
|
264
264
|
key = Regexp.last_match[1]
|
|
265
|
-
|
|
265
|
+
_redefine_method(key) if _method?(key) && !_already_overridden?(key)
|
|
266
266
|
return self[convert_key(key)] = args.first
|
|
267
267
|
end
|
|
268
268
|
|
|
@@ -279,10 +279,10 @@ module Hashie
|
|
|
279
279
|
super
|
|
280
280
|
end
|
|
281
281
|
|
|
282
|
-
|
|
282
|
+
private
|
|
283
283
|
|
|
284
|
-
def
|
|
285
|
-
|
|
284
|
+
def _already_overridden?(name)
|
|
285
|
+
_method?("__#{name}")
|
|
286
286
|
end
|
|
287
287
|
end
|
|
288
288
|
|
|
@@ -344,7 +344,7 @@ module Hashie
|
|
|
344
344
|
|
|
345
345
|
hash.each do |key, value|
|
|
346
346
|
skey = key.to_s
|
|
347
|
-
|
|
347
|
+
_redefine_method(skey) if _method?(skey)
|
|
348
348
|
self[skey] = value
|
|
349
349
|
end
|
|
350
350
|
end
|
|
@@ -48,7 +48,7 @@ module Hashie
|
|
|
48
48
|
# file.close!
|
|
49
49
|
# end
|
|
50
50
|
def perform
|
|
51
|
-
|
|
51
|
+
_yaml_safe_load(@content)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# @example Load YAML in one step
|
|
@@ -67,7 +67,7 @@ module Hashie
|
|
|
67
67
|
|
|
68
68
|
private
|
|
69
69
|
|
|
70
|
-
def
|
|
70
|
+
def _yaml_safe_load(content)
|
|
71
71
|
YAML.safe_load(
|
|
72
72
|
content,
|
|
73
73
|
permitted_classes: @options.fetch(:permitted_classes) { [] },
|
|
@@ -98,7 +98,7 @@ module Hashie
|
|
|
98
98
|
raise ArgumentError, 'a persistence path is required' if path.nil?
|
|
99
99
|
|
|
100
100
|
path = File.expand_path(File.path(path))
|
|
101
|
-
yaml = Psych.dump(
|
|
101
|
+
yaml = Psych.dump(_persistable_data(self))
|
|
102
102
|
Tempfile.create(['.hashie-', '.yaml'], File.dirname(path)) do |file|
|
|
103
103
|
file.chmod(File.stat(path).mode & 0o777) if File.exist?(path)
|
|
104
104
|
file.write(yaml)
|
|
@@ -112,7 +112,7 @@ module Hashie
|
|
|
112
112
|
|
|
113
113
|
private
|
|
114
114
|
|
|
115
|
-
def
|
|
115
|
+
def _persistable_data(value, ancestors = [])
|
|
116
116
|
raise ArgumentError, 'cannot persist cyclic data' if ancestors.any? { |ancestor| ancestor.equal?(value) }
|
|
117
117
|
|
|
118
118
|
case value
|
|
@@ -120,10 +120,10 @@ module Hashie
|
|
|
120
120
|
value.each_with_object({}) do |(key, child), data|
|
|
121
121
|
raise TypeError, 'persisted hash keys must be strings or symbols' unless key.is_a?(String) || key.is_a?(Symbol)
|
|
122
122
|
|
|
123
|
-
data[
|
|
123
|
+
data[_persistable_data(key)] = _persistable_data(child, ancestors + [value])
|
|
124
124
|
end
|
|
125
125
|
when ::Array
|
|
126
|
-
value.map { |child|
|
|
126
|
+
value.map { |child| _persistable_data(child, ancestors + [value]) }
|
|
127
127
|
when String
|
|
128
128
|
value.dup
|
|
129
129
|
when Symbol, Integer, Float, TrueClass, FalseClass, NilClass
|
|
@@ -32,7 +32,7 @@ module Hashie
|
|
|
32
32
|
# version = Hashie::Extensions::RubyVersion.new('3.2.0.rc1')
|
|
33
33
|
# version.segments # => [3, 2, 0, 'rc', 1]
|
|
34
34
|
def initialize(version)
|
|
35
|
-
@segments =
|
|
35
|
+
@segments = _split_to_segments(version)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# @example Ignore trailing zeros and order prereleases before releases
|
|
@@ -67,7 +67,7 @@ module Hashie
|
|
|
67
67
|
|
|
68
68
|
private
|
|
69
69
|
|
|
70
|
-
def
|
|
70
|
+
def _split_to_segments(version)
|
|
71
71
|
version.scan(/[0-9]+|[a-z]+/i).map do |segment|
|
|
72
72
|
/^\d+$/ =~ segment ? segment.to_i : segment
|
|
73
73
|
end.freeze
|
|
@@ -68,16 +68,17 @@ module Hashie
|
|
|
68
68
|
# Stringify all keys recursively within nested
|
|
69
69
|
# hashes and arrays.
|
|
70
70
|
# @api private
|
|
71
|
-
def
|
|
71
|
+
def _stringify_keys_recursively!(object)
|
|
72
72
|
case object
|
|
73
73
|
when ::Array
|
|
74
74
|
object.each do |i|
|
|
75
|
-
|
|
75
|
+
_stringify_keys_recursively!(i)
|
|
76
76
|
end
|
|
77
77
|
when ::Hash
|
|
78
78
|
stringify_keys!(object)
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
|
+
private :_stringify_keys_recursively!
|
|
81
82
|
|
|
82
83
|
# Converts all keys in a hash recursively to strings, in place.
|
|
83
84
|
#
|
|
@@ -92,7 +93,7 @@ module Hashie
|
|
|
92
93
|
def stringify_keys!(hash)
|
|
93
94
|
hash.extend(Hashie::Extensions::StringifyKeys) unless hash.respond_to?(:stringify_keys!)
|
|
94
95
|
hash.keys.each do |k| # rubocop:disable Performance/HashEachMethods
|
|
95
|
-
|
|
96
|
+
_stringify_keys_recursively!(hash[k])
|
|
96
97
|
hash[k.to_s] = hash.delete(k)
|
|
97
98
|
end
|
|
98
99
|
hash
|
|
@@ -69,16 +69,17 @@ module Hashie
|
|
|
69
69
|
# Symbolize all keys recursively within nested
|
|
70
70
|
# hashes and arrays.
|
|
71
71
|
# @api private
|
|
72
|
-
def
|
|
72
|
+
def _symbolize_keys_recursively!(object)
|
|
73
73
|
case object
|
|
74
74
|
when ::Array
|
|
75
75
|
object.each do |i|
|
|
76
|
-
|
|
76
|
+
_symbolize_keys_recursively!(i)
|
|
77
77
|
end
|
|
78
78
|
when ::Hash
|
|
79
79
|
symbolize_keys!(object)
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
|
+
private :_symbolize_keys_recursively!
|
|
82
83
|
|
|
83
84
|
# Converts keys in a hash recursively to symbols, in place.
|
|
84
85
|
#
|
|
@@ -93,7 +94,7 @@ module Hashie
|
|
|
93
94
|
def symbolize_keys!(hash)
|
|
94
95
|
hash.extend(Hashie::Extensions::SymbolizeKeys) unless hash.respond_to?(:symbolize_keys!)
|
|
95
96
|
hash.keys.each do |k| # rubocop:disable Performance/HashEachMethods
|
|
96
|
-
|
|
97
|
+
_symbolize_keys_recursively!(hash[k])
|
|
97
98
|
hash[convert_key(k)] = hash.delete(k)
|
|
98
99
|
end
|
|
99
100
|
hash
|
data/lib/hashie/hash.rb
CHANGED
|
@@ -41,10 +41,10 @@ module Hashie
|
|
|
41
41
|
if self[k].is_a?(Array)
|
|
42
42
|
out[assignment_key] ||= []
|
|
43
43
|
self[k].each do |array_object|
|
|
44
|
-
out[assignment_key] <<
|
|
44
|
+
out[assignment_key] << _maybe_convert_to_hash(array_object, options)
|
|
45
45
|
end
|
|
46
46
|
else
|
|
47
|
-
out[assignment_key] =
|
|
47
|
+
out[assignment_key] = _maybe_convert_to_hash(self[k], options)
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
out
|
|
@@ -61,13 +61,13 @@ module Hashie
|
|
|
61
61
|
|
|
62
62
|
private
|
|
63
63
|
|
|
64
|
-
def
|
|
64
|
+
def _maybe_convert_to_hash(object, options)
|
|
65
65
|
return object unless object.is_a?(Hash) || object.respond_to?(:to_hash)
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
_flexibly_convert_to_hash(object, options)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
def
|
|
70
|
+
def _flexibly_convert_to_hash(object, options = {})
|
|
71
71
|
if object.method(:to_hash).arity.zero?
|
|
72
72
|
object.to_hash
|
|
73
73
|
else
|
data/lib/hashie/mash.rb
CHANGED
|
@@ -72,8 +72,8 @@ module Hashie
|
|
|
72
72
|
|
|
73
73
|
ALLOWED_SUFFIXES = %w[? ! = _].freeze
|
|
74
74
|
|
|
75
|
-
# Loads
|
|
76
|
-
#
|
|
75
|
+
# Loads YAML without evaluating ERB. Pass YamlErbParser explicitly to
|
|
76
|
+
# evaluate templates from trusted files.
|
|
77
77
|
#
|
|
78
78
|
# @example Load and cache a frozen configuration from a YAML file
|
|
79
79
|
# require 'tempfile'
|
|
@@ -96,7 +96,7 @@ module Hashie
|
|
|
96
96
|
raise ArgumentError, "file not found: `#{path}`" unless File.file?(path)
|
|
97
97
|
|
|
98
98
|
options = options.dup
|
|
99
|
-
parser = options.delete(:parser) { Hashie::Extensions::Parsers::
|
|
99
|
+
parser = options.delete(:parser) { Hashie::Extensions::Parsers::YamlParser }
|
|
100
100
|
@_mashes[path] = new(parser.perform(path, options)).freeze
|
|
101
101
|
end
|
|
102
102
|
|
|
@@ -128,8 +128,8 @@ module Hashie
|
|
|
128
128
|
|
|
129
129
|
# If you pass in an existing hash, it will
|
|
130
130
|
# convert it to a Mash including recursively
|
|
131
|
-
# descending into arrays and hashes, converting
|
|
132
|
-
# them as well.
|
|
131
|
+
# descending into arrays and plain hashes, converting
|
|
132
|
+
# them as well. Nested custom Hash subclasses retain their identity.
|
|
133
133
|
#
|
|
134
134
|
# @example Recursively convert hashes nested in arrays
|
|
135
135
|
# mash = Hashie::Mash.new(users: [{ name: 'Ada' }])
|
|
@@ -202,7 +202,7 @@ module Hashie
|
|
|
202
202
|
# mash.keys # => ['user']
|
|
203
203
|
# mash.user.name # => 'Ada'
|
|
204
204
|
def custom_writer(key, value, convert = true) # :nodoc:
|
|
205
|
-
|
|
205
|
+
_log_built_in_message(key) if key.respond_to?(:to_sym) && _log_collision?(key.to_sym)
|
|
206
206
|
regular_writer(convert_key(key), convert ? convert_value(value) : value)
|
|
207
207
|
end
|
|
208
208
|
|
|
@@ -410,7 +410,7 @@ module Hashie
|
|
|
410
410
|
def _deep_update(other_hash, &blk)
|
|
411
411
|
other_hash.each_pair do |k, v|
|
|
412
412
|
key = convert_key(k)
|
|
413
|
-
if v.
|
|
413
|
+
if (v.instance_of?(::Hash) || v.is_a?(Mash)) && key?(key) && regular_reader(key).is_a?(Mash)
|
|
414
414
|
custom_reader(key).deep_update(v, &blk)
|
|
415
415
|
else
|
|
416
416
|
value = convert_value(v, true)
|
|
@@ -473,7 +473,7 @@ module Hashie
|
|
|
473
473
|
def respond_to_missing?(method_name, *args)
|
|
474
474
|
return true if key?(method_name)
|
|
475
475
|
|
|
476
|
-
suffix =
|
|
476
|
+
suffix = _method_suffix(method_name)
|
|
477
477
|
if suffix
|
|
478
478
|
true
|
|
479
479
|
else
|
|
@@ -500,7 +500,7 @@ module Hashie
|
|
|
500
500
|
def method_missing(method_name, *args, &blk)
|
|
501
501
|
return self.[](method_name, &blk) if key?(method_name)
|
|
502
502
|
|
|
503
|
-
name, suffix =
|
|
503
|
+
name, suffix = _method_name_and_suffix(method_name)
|
|
504
504
|
case suffix
|
|
505
505
|
when '='
|
|
506
506
|
assign_property(name, args.first)
|
|
@@ -581,9 +581,9 @@ module Hashie
|
|
|
581
581
|
self.class.new(super(*string_keys))
|
|
582
582
|
end
|
|
583
583
|
|
|
584
|
-
|
|
584
|
+
private
|
|
585
585
|
|
|
586
|
-
def
|
|
586
|
+
def _method_name_and_suffix(method_name)
|
|
587
587
|
method_name = method_name.to_s
|
|
588
588
|
if method_name.end_with?(*ALLOWED_SUFFIXES)
|
|
589
589
|
[method_name[0..-2], method_name[-1]]
|
|
@@ -592,11 +592,13 @@ module Hashie
|
|
|
592
592
|
end
|
|
593
593
|
end
|
|
594
594
|
|
|
595
|
-
def
|
|
595
|
+
def _method_suffix(method_name)
|
|
596
596
|
method_name = method_name.to_s
|
|
597
597
|
method_name[-1] if method_name.end_with?(*ALLOWED_SUFFIXES)
|
|
598
598
|
end
|
|
599
599
|
|
|
600
|
+
protected
|
|
601
|
+
|
|
600
602
|
# Symbol#name returns the symbol's interned frozen string, where
|
|
601
603
|
# Symbol#to_s allocates a new one on every read and write.
|
|
602
604
|
def convert_key(key) # :nodoc:
|
|
@@ -609,9 +611,11 @@ module Hashie
|
|
|
609
611
|
case val
|
|
610
612
|
when self.class
|
|
611
613
|
val.dup
|
|
612
|
-
when
|
|
614
|
+
when Mash
|
|
613
615
|
duping ? val.dup : val
|
|
614
616
|
when ::Hash
|
|
617
|
+
return val unless val.instance_of?(::Hash)
|
|
618
|
+
|
|
615
619
|
val = val.dup if duping
|
|
616
620
|
self.class.new(val)
|
|
617
621
|
when ::Array
|
|
@@ -623,7 +627,7 @@ module Hashie
|
|
|
623
627
|
|
|
624
628
|
private
|
|
625
629
|
|
|
626
|
-
def
|
|
630
|
+
def _log_built_in_message(method_key)
|
|
627
631
|
return if self.class.disable_warnings?(method_key)
|
|
628
632
|
|
|
629
633
|
method_information = Hashie::Utils.method_information(method(method_key))
|
|
@@ -636,11 +640,11 @@ module Hashie
|
|
|
636
640
|
)
|
|
637
641
|
end
|
|
638
642
|
|
|
639
|
-
def
|
|
643
|
+
def _log_collision?(method_key)
|
|
640
644
|
return false unless method_key.is_a?(String) || method_key.is_a?(Symbol)
|
|
641
645
|
return false unless respond_to?(method_key)
|
|
642
646
|
|
|
643
|
-
_, suffix =
|
|
647
|
+
_, suffix = _method_name_and_suffix(method_key)
|
|
644
648
|
|
|
645
649
|
(!suffix || suffix == '=') &&
|
|
646
650
|
!self.class.disable_warnings?(method_key) &&
|
data/lib/hashie/rash.rb
CHANGED
|
@@ -145,7 +145,7 @@ module Hashie
|
|
|
145
145
|
|
|
146
146
|
case query
|
|
147
147
|
when String
|
|
148
|
-
|
|
148
|
+
_optimize_if_necessary!
|
|
149
149
|
|
|
150
150
|
# see if any of the regexps match the string
|
|
151
151
|
@regexes.each do |regex|
|
|
@@ -193,7 +193,7 @@ module Hashie
|
|
|
193
193
|
|
|
194
194
|
private
|
|
195
195
|
|
|
196
|
-
def
|
|
196
|
+
def _optimize_if_necessary!
|
|
197
197
|
return unless (@lookups += 1) >= @optimize_every
|
|
198
198
|
|
|
199
199
|
@regexes = @regexes.sort_by { |regex| -@regex_counts[regex] }
|
data/lib/hashie/utils.rb
CHANGED
|
@@ -37,7 +37,7 @@ module Hashie
|
|
|
37
37
|
# Hashie::Utils.safe_dup(:name) # => :name
|
|
38
38
|
def self.safe_dup(value)
|
|
39
39
|
case value
|
|
40
|
-
when Complex, FalseClass, NilClass, Rational, Method, Symbol, TrueClass, *
|
|
40
|
+
when Complex, FalseClass, NilClass, Rational, Method, Symbol, TrueClass, *_integer_classes
|
|
41
41
|
value
|
|
42
42
|
else
|
|
43
43
|
value.dup
|
|
@@ -48,8 +48,9 @@ module Hashie
|
|
|
48
48
|
#
|
|
49
49
|
# @api private
|
|
50
50
|
# @return [Array<Class>]
|
|
51
|
-
def self.
|
|
51
|
+
def self._integer_classes
|
|
52
52
|
@integer_classes ||= [Integer]
|
|
53
53
|
end
|
|
54
|
+
private_class_method :_integer_classes
|
|
54
55
|
end
|
|
55
56
|
end
|
data/lib/hashie/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hashie
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 6.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michael Bleigh
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-
|
|
12
|
+
date: 2026-11-11 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: logger
|
|
@@ -117,7 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
117
117
|
- !ruby/object:Gem::Version
|
|
118
118
|
version: '0'
|
|
119
119
|
requirements: []
|
|
120
|
-
rubygems_version: 3.5.
|
|
120
|
+
rubygems_version: 3.5.22
|
|
121
121
|
signing_key:
|
|
122
122
|
specification_version: 4
|
|
123
123
|
summary: Your friendly neighborhood hash library.
|