hashie 5.1.0 → 5.2.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 +56 -0
- data/CONTRIBUTING.md +38 -1
- data/README.md +816 -181
- data/Rakefile +29 -1
- data/UPGRADING.md +18 -0
- data/hashie.gemspec +8 -7
- data/lib/hashie/array.rb +15 -12
- data/lib/hashie/clash.rb +59 -4
- data/lib/hashie/dash.rb +144 -16
- data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
- data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
- data/lib/hashie/extensions/block_access.rb +71 -0
- data/lib/hashie/extensions/coercion.rb +180 -19
- data/lib/hashie/extensions/dash/coercion.rb +34 -3
- data/lib/hashie/extensions/dash/indifferent_access.rb +83 -5
- data/lib/hashie/extensions/dash/lazy.rb +170 -0
- data/lib/hashie/extensions/dash/predefined_values.rb +69 -9
- data/lib/hashie/extensions/dash/property_translation.rb +134 -16
- data/lib/hashie/extensions/deep_fetch.rb +21 -9
- data/lib/hashie/extensions/deep_find.rb +20 -0
- data/lib/hashie/extensions/deep_grep.rb +69 -0
- data/lib/hashie/extensions/deep_locate.rb +15 -28
- data/lib/hashie/extensions/deep_merge.rb +16 -0
- data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
- data/lib/hashie/extensions/ignore_undeclared.rb +21 -3
- data/lib/hashie/extensions/indifferent_access.rb +100 -14
- data/lib/hashie/extensions/kernel_coercion.rb +58 -0
- data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
- data/lib/hashie/extensions/key_conversion.rb +9 -0
- data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
- data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
- data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
- data/lib/hashie/extensions/mash/required_keys.rb +111 -0
- data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
- data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
- data/lib/hashie/extensions/mash/symbolize_keys.rb +6 -2
- data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
- data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
- data/lib/hashie/extensions/merge_initializer.rb +9 -0
- data/lib/hashie/extensions/method_access.rb +93 -7
- data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
- data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
- data/lib/hashie/extensions/persistable.rb +137 -0
- data/lib/hashie/extensions/pretty_inspect.rb +14 -0
- data/lib/hashie/extensions/ruby_version.rb +18 -1
- data/lib/hashie/extensions/ruby_version_check.rb +17 -0
- data/lib/hashie/extensions/strict_key_access.rb +59 -5
- data/lib/hashie/extensions/stringify_keys.rb +72 -17
- data/lib/hashie/extensions/symbolize_keys.rb +72 -16
- data/lib/hashie/hash.rb +16 -0
- data/lib/hashie/logger.rb +18 -1
- data/lib/hashie/mash.rb +293 -62
- data/lib/hashie/railtie.rb +27 -1
- data/lib/hashie/rash.rb +64 -4
- data/lib/hashie/trash.rb +8 -0
- data/lib/hashie/utils.rb +17 -6
- data/lib/hashie/version.rb +3 -1
- data/lib/hashie.rb +45 -0
- metadata +15 -17
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
# MethodReader allows you to access keys of the hash
|
|
@@ -25,13 +27,22 @@ module Hashie
|
|
|
25
27
|
# user[:birthday] = nil
|
|
26
28
|
# user.birthday # => nil
|
|
27
29
|
#
|
|
28
|
-
# user.not_declared
|
|
30
|
+
# expect { user.not_declared }.to raise_error(NoMethodError)
|
|
29
31
|
module MethodReader
|
|
32
|
+
# @example Advertise readers for existing keys
|
|
33
|
+
# hash = { name: 'Ada' }.extend(Hashie::Extensions::MethodReader)
|
|
34
|
+
# hash.respond_to?(:name) # => true
|
|
35
|
+
# hash.respond_to?(:missing) # => false
|
|
30
36
|
def respond_to_missing?(name, include_private = false)
|
|
31
37
|
return true if key?(name.to_s) || key?(name.to_sym)
|
|
38
|
+
|
|
32
39
|
super
|
|
33
40
|
end
|
|
34
41
|
|
|
42
|
+
# @example Read both symbol and string keys
|
|
43
|
+
# hash = { name: 'Ada', 'age' => 42 }.extend(Hashie::Extensions::MethodReader)
|
|
44
|
+
# hash.name # => 'Ada'
|
|
45
|
+
# hash.age # => 42
|
|
35
46
|
def method_missing(name, *args)
|
|
36
47
|
if key?(name)
|
|
37
48
|
self[name]
|
|
@@ -67,19 +78,29 @@ module Hashie
|
|
|
67
78
|
# h['awesome'] # => 'sauce'
|
|
68
79
|
#
|
|
69
80
|
module MethodWriter
|
|
81
|
+
# @example Advertise arbitrary writers
|
|
82
|
+
# hash = {}.extend(Hashie::Extensions::MethodWriter)
|
|
83
|
+
# hash.respond_to?(:name=) # => true
|
|
84
|
+
# hash.respond_to?(:name) # => false
|
|
70
85
|
def respond_to_missing?(name, include_private = false)
|
|
71
86
|
return true if name.to_s =~ /=$/
|
|
87
|
+
|
|
72
88
|
super
|
|
73
89
|
end
|
|
74
90
|
|
|
91
|
+
# @example Write string keys with assignment syntax
|
|
92
|
+
# hash = {}.extend(Hashie::Extensions::MethodWriter)
|
|
93
|
+
# hash.name = 'Ada'
|
|
94
|
+
# hash # => { 'name' => 'Ada' }
|
|
75
95
|
def method_missing(name, *args)
|
|
76
|
-
if args.size == 1 && name.to_s =~ /(.*)=$/
|
|
77
|
-
return self[convert_key(Regexp.last_match[1])] = args.first
|
|
78
|
-
end
|
|
96
|
+
return self[convert_key(Regexp.last_match[1])] = args.first if args.size == 1 && name.to_s =~ /(.*)=$/
|
|
79
97
|
|
|
80
98
|
super
|
|
81
99
|
end
|
|
82
100
|
|
|
101
|
+
# @example Normalize a writer key
|
|
102
|
+
# hash = {}.extend(Hashie::Extensions::MethodWriter)
|
|
103
|
+
# hash.convert_key(:name) # => 'name'
|
|
83
104
|
def convert_key(key)
|
|
84
105
|
key.to_s
|
|
85
106
|
end
|
|
@@ -104,8 +125,12 @@ module Hashie
|
|
|
104
125
|
# h.abc? # => true
|
|
105
126
|
# h['def'] = nil
|
|
106
127
|
# h.def? # => false
|
|
107
|
-
# h.hji?
|
|
128
|
+
# expect { h.hji? }.to raise_error(NoMethodError)
|
|
108
129
|
module MethodQuery
|
|
130
|
+
# @example Advertise queries for present keys, including nil values
|
|
131
|
+
# hash = { name: nil }.extend(Hashie::Extensions::MethodQuery)
|
|
132
|
+
# hash.respond_to?(:name?) # => true
|
|
133
|
+
# hash.respond_to?(:missing?) # => false
|
|
109
134
|
def respond_to_missing?(name, include_private = false)
|
|
110
135
|
if query_method?(name) && indifferent_key?(key_from_query_method(name))
|
|
111
136
|
true
|
|
@@ -114,6 +139,10 @@ module Hashie
|
|
|
114
139
|
end
|
|
115
140
|
end
|
|
116
141
|
|
|
142
|
+
# @example Query the truthiness of values
|
|
143
|
+
# hash = { active: true, disabled: nil }.extend(Hashie::Extensions::MethodQuery)
|
|
144
|
+
# hash.active? # => true
|
|
145
|
+
# hash.disabled? # => false
|
|
117
146
|
def method_missing(name, *args)
|
|
118
147
|
return super unless args.empty?
|
|
119
148
|
|
|
@@ -148,6 +177,12 @@ module Hashie
|
|
|
148
177
|
# A macro module that will automatically include MethodReader,
|
|
149
178
|
# MethodWriter, and MethodQuery, giving you the ability to read,
|
|
150
179
|
# write, and query keys in a hash using method call shortcuts.
|
|
180
|
+
#
|
|
181
|
+
# @example Read, write, and query through methods
|
|
182
|
+
# hash = {}.extend(Module.new { include Hashie::Extensions::MethodAccess })
|
|
183
|
+
# hash.name = 'Ada'
|
|
184
|
+
# hash.name # => 'Ada'
|
|
185
|
+
# hash.name? # => true
|
|
151
186
|
module MethodAccess
|
|
152
187
|
def self.included(base)
|
|
153
188
|
[MethodReader, MethodWriter, MethodQuery].each do |mod|
|
|
@@ -158,6 +193,16 @@ module Hashie
|
|
|
158
193
|
|
|
159
194
|
# A module shared between MethodOverridingWriter and MethodOverridingInitializer
|
|
160
195
|
# to contained shared logic. This module aids in redefining existing hash methods.
|
|
196
|
+
#
|
|
197
|
+
# @example Preserve an overridden method under a double-underscore alias
|
|
198
|
+
# require 'hashie/extensions/method_access'
|
|
199
|
+
# class OverridingHash < Hash
|
|
200
|
+
# include Hashie::Extensions::MethodOverridingWriter
|
|
201
|
+
# end
|
|
202
|
+
# hash = OverridingHash.new
|
|
203
|
+
# hash.size = 'large'
|
|
204
|
+
# hash.size # => 'large'
|
|
205
|
+
# hash.__size # => 1
|
|
161
206
|
module RedefineMethod
|
|
162
207
|
protected
|
|
163
208
|
|
|
@@ -185,6 +230,7 @@ module Hashie
|
|
|
185
230
|
# appropriately as true.
|
|
186
231
|
#
|
|
187
232
|
# @example
|
|
233
|
+
# require 'hashie/extensions/method_access'
|
|
188
234
|
# class MyHash < Hash
|
|
189
235
|
# include Hashie::Extensions::MethodOverridingWriter
|
|
190
236
|
# end
|
|
@@ -194,15 +240,25 @@ module Hashie
|
|
|
194
240
|
# h['awesome'] # => 'sauce'
|
|
195
241
|
# h.zip = 'a-dee-doo-dah'
|
|
196
242
|
# h.zip # => 'a-dee-doo-dah'
|
|
197
|
-
# h.__zip # => [[['awesome', 'sauce'], ['zip', 'a-dee-doo-dah']]]
|
|
243
|
+
# h.__zip # => [[['awesome', 'sauce']], [['zip', 'a-dee-doo-dah']]]
|
|
198
244
|
#
|
|
199
245
|
module MethodOverridingWriter
|
|
200
246
|
include RedefineMethod
|
|
201
247
|
|
|
248
|
+
# @example Normalize keys before storage
|
|
249
|
+
# require 'hashie/extensions/method_access'
|
|
250
|
+
# hash = {}.extend(Hashie::Extensions::MethodOverridingWriter)
|
|
251
|
+
# hash.convert_key(:size) # => 'size'
|
|
202
252
|
def convert_key(key)
|
|
203
253
|
key.to_s
|
|
204
254
|
end
|
|
205
255
|
|
|
256
|
+
# @example Override a Hash method and keep access to the original
|
|
257
|
+
# require 'hashie/extensions/method_access'
|
|
258
|
+
# hash = {}.extend(Hashie::Extensions::MethodOverridingWriter)
|
|
259
|
+
# hash.size = 'large'
|
|
260
|
+
# hash.size # => 'large'
|
|
261
|
+
# hash.__size # => 1
|
|
206
262
|
def method_missing(name, *args)
|
|
207
263
|
if args.size == 1 && name.to_s =~ /(.*)=$/
|
|
208
264
|
key = Regexp.last_match[1]
|
|
@@ -213,8 +269,13 @@ module Hashie
|
|
|
213
269
|
super
|
|
214
270
|
end
|
|
215
271
|
|
|
272
|
+
# @example Advertise method-overriding writers
|
|
273
|
+
# require 'hashie/extensions/method_access'
|
|
274
|
+
# hash = {}.extend(Hashie::Extensions::MethodOverridingWriter)
|
|
275
|
+
# hash.respond_to?(:size=) # => true
|
|
216
276
|
def respond_to_missing?(name, include_private = false)
|
|
217
277
|
return true if name.to_s.end_with?('=')
|
|
278
|
+
|
|
218
279
|
super
|
|
219
280
|
end
|
|
220
281
|
|
|
@@ -231,6 +292,16 @@ module Hashie
|
|
|
231
292
|
# shortcuts that can override object methods. Any overridden
|
|
232
293
|
# object method is automatically aliased with two leading
|
|
233
294
|
# underscores.
|
|
295
|
+
#
|
|
296
|
+
# @example Initialize, read, write, and query a conflicting key
|
|
297
|
+
# class OverridingHash < Hash
|
|
298
|
+
# include Hashie::Extensions::MethodAccessWithOverride
|
|
299
|
+
# end
|
|
300
|
+
# hash = OverridingHash.new(size: 'large')
|
|
301
|
+
# hash.size # => 'large'
|
|
302
|
+
# hash.size = 'small'
|
|
303
|
+
# hash.size? # => true
|
|
304
|
+
# hash.__size # => 1
|
|
234
305
|
module MethodAccessWithOverride
|
|
235
306
|
def self.included(base)
|
|
236
307
|
[MethodReader, MethodOverridingWriter,
|
|
@@ -245,6 +316,7 @@ module Hashie
|
|
|
245
316
|
# methods are aliased with two leading underscores.
|
|
246
317
|
#
|
|
247
318
|
# @example
|
|
319
|
+
# require 'hashie/extensions/method_access'
|
|
248
320
|
# class MyHash < Hash
|
|
249
321
|
# include Hashie::Extensions::MethodOverridingInitializer
|
|
250
322
|
# end
|
|
@@ -255,7 +327,21 @@ module Hashie
|
|
|
255
327
|
module MethodOverridingInitializer
|
|
256
328
|
include RedefineMethod
|
|
257
329
|
|
|
258
|
-
|
|
330
|
+
# @example Initialize conflicting keys and preserve a default
|
|
331
|
+
# require 'hashie/extensions/method_access'
|
|
332
|
+
# class OverridingHash < Hash
|
|
333
|
+
# include Hashie::Extensions::MethodOverridingInitializer
|
|
334
|
+
# end
|
|
335
|
+
# hash = OverridingHash.new({ size: 'large' }, false)
|
|
336
|
+
# hash.size # => 'large'
|
|
337
|
+
# hash.__size # => 1
|
|
338
|
+
# hash[:missing] # => false
|
|
339
|
+
def initialize(hash = {}, default = nil, &block)
|
|
340
|
+
raise ArgumentError, 'cannot specify both a default value and a default block' if default && block
|
|
341
|
+
|
|
342
|
+
self.default = default unless default.nil?
|
|
343
|
+
self.default_proc = block if block
|
|
344
|
+
|
|
259
345
|
hash.each do |key, value|
|
|
260
346
|
skey = key.to_s
|
|
261
347
|
redefine_method(skey) if method?(skey)
|
|
@@ -1,46 +1,39 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require 'erb'
|
|
3
|
-
require '
|
|
4
|
+
require 'hashie/extensions/parsers/yaml_parser'
|
|
4
5
|
|
|
5
6
|
module Hashie
|
|
6
7
|
module Extensions
|
|
7
8
|
module Parsers
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
# Parses YAML after evaluating ERB templates.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# require 'tempfile'
|
|
13
|
+
# file = Tempfile.new(['hashie-example', '.yml'], '.')
|
|
14
|
+
# begin
|
|
15
|
+
# file.write("count: <%= 1 + 2 %>\n")
|
|
16
|
+
# file.close
|
|
17
|
+
# Hashie::Extensions::Parsers::YamlErbParser.perform(file.path) # => {'count' => 3}
|
|
18
|
+
# ensure
|
|
19
|
+
# file.close!
|
|
20
|
+
# end
|
|
21
|
+
class YamlErbParser < YamlParser
|
|
22
|
+
# @example Evaluate ERB before safely loading YAML
|
|
23
|
+
# require 'tempfile'
|
|
24
|
+
# file = Tempfile.new(['hashie-example', '.yml'], '.')
|
|
25
|
+
# begin
|
|
26
|
+
# file.write("count: <%= 2 * 3 %>\n")
|
|
27
|
+
# file.close
|
|
28
|
+
# parser = Hashie::Extensions::Parsers::YamlErbParser.new(file.path)
|
|
29
|
+
# parser.perform # => {'count' => 6}
|
|
30
|
+
# ensure
|
|
31
|
+
# file.close!
|
|
32
|
+
# end
|
|
15
33
|
def perform
|
|
16
34
|
template = ERB.new(@content)
|
|
17
35
|
template.filename = @file_path
|
|
18
|
-
|
|
19
|
-
permitted_symbols = @options.fetch(:permitted_symbols) { [] }
|
|
20
|
-
aliases = @options.fetch(:aliases) { true }
|
|
21
|
-
|
|
22
|
-
yaml_safe_load(template, permitted_classes, permitted_symbols, aliases)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def self.perform(file_path, options = {})
|
|
26
|
-
new(file_path, options).perform
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.1.0') # Ruby 2.6+
|
|
32
|
-
def yaml_safe_load(template, permitted_classes, permitted_symbols, aliases)
|
|
33
|
-
YAML.safe_load(
|
|
34
|
-
template.result,
|
|
35
|
-
permitted_classes: permitted_classes,
|
|
36
|
-
permitted_symbols: permitted_symbols,
|
|
37
|
-
aliases: aliases
|
|
38
|
-
)
|
|
39
|
-
end
|
|
40
|
-
else
|
|
41
|
-
def yaml_safe_load(template, permitted_classes, permitted_symbols, aliases)
|
|
42
|
-
YAML.safe_load(template.result, permitted_classes, permitted_symbols, aliases)
|
|
43
|
-
end
|
|
36
|
+
yaml_safe_load(template.result)
|
|
44
37
|
end
|
|
45
38
|
end
|
|
46
39
|
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
module Hashie
|
|
7
|
+
module Extensions
|
|
8
|
+
module Parsers
|
|
9
|
+
# Parses YAML without evaluating ERB templates.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# require 'tempfile'
|
|
13
|
+
# file = Tempfile.new(['hashie-example', '.yml'], '.')
|
|
14
|
+
# begin
|
|
15
|
+
# file.write("name: Ada\n")
|
|
16
|
+
# file.close
|
|
17
|
+
# Hashie::Extensions::Parsers::YamlParser.perform(file.path) # => {'name' => 'Ada'}
|
|
18
|
+
# ensure
|
|
19
|
+
# file.close!
|
|
20
|
+
# end
|
|
21
|
+
class YamlParser
|
|
22
|
+
# @example Read a Pathname and retain parsing options
|
|
23
|
+
# require 'tempfile'
|
|
24
|
+
# file = Tempfile.new(['hashie-example', '.yml'], '.')
|
|
25
|
+
# begin
|
|
26
|
+
# file.write("name: Ada\n")
|
|
27
|
+
# file.close
|
|
28
|
+
# parser = Hashie::Extensions::Parsers::YamlParser.new(Pathname.new(file.path), aliases: false)
|
|
29
|
+
# parser.perform # => {'name' => 'Ada'}
|
|
30
|
+
# ensure
|
|
31
|
+
# file.close!
|
|
32
|
+
# end
|
|
33
|
+
def initialize(file_path, options = {})
|
|
34
|
+
@content = File.read(file_path)
|
|
35
|
+
@file_path = file_path.is_a?(Pathname) ? file_path.to_s : file_path
|
|
36
|
+
@options = options
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @example Leave ERB untouched when loading YAML
|
|
40
|
+
# require 'tempfile'
|
|
41
|
+
# file = Tempfile.new(['hashie-example', '.yml'], '.')
|
|
42
|
+
# begin
|
|
43
|
+
# file.write("value: <%= 1 + 2 %>\n")
|
|
44
|
+
# file.close
|
|
45
|
+
# parser = Hashie::Extensions::Parsers::YamlParser.new(file.path)
|
|
46
|
+
# parser.perform # => {'value' => '<%= 1 + 2 %>'}
|
|
47
|
+
# ensure
|
|
48
|
+
# file.close!
|
|
49
|
+
# end
|
|
50
|
+
def perform
|
|
51
|
+
yaml_safe_load(@content)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @example Load YAML in one step
|
|
55
|
+
# require 'tempfile'
|
|
56
|
+
# file = Tempfile.new(['hashie-example', '.yml'], '.')
|
|
57
|
+
# begin
|
|
58
|
+
# file.write("count: 3\n")
|
|
59
|
+
# file.close
|
|
60
|
+
# Hashie::Extensions::Parsers::YamlParser.perform(file.path, aliases: false) # => {'count' => 3}
|
|
61
|
+
# ensure
|
|
62
|
+
# file.close!
|
|
63
|
+
# end
|
|
64
|
+
def self.perform(file_path, options = {})
|
|
65
|
+
new(file_path, options).perform
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def yaml_safe_load(content)
|
|
71
|
+
YAML.safe_load(
|
|
72
|
+
content,
|
|
73
|
+
permitted_classes: @options.fetch(:permitted_classes) { [] },
|
|
74
|
+
permitted_symbols: @options.fetch(:permitted_symbols) { [] },
|
|
75
|
+
aliases: @options.fetch(:aliases, true)
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'psych'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
|
|
6
|
+
module Hashie
|
|
7
|
+
module Extensions
|
|
8
|
+
# Opt-in YAML file persistence for classes initialized from a hash.
|
|
9
|
+
#
|
|
10
|
+
# @example Save and reload a hash subclass
|
|
11
|
+
# class Settings < Hash
|
|
12
|
+
# include Hashie::Extensions::MergeInitializer
|
|
13
|
+
# include Hashie::Extensions::Persistable
|
|
14
|
+
# end
|
|
15
|
+
# file = Tempfile.new(['hashie-example', '.yaml'], Dir.pwd)
|
|
16
|
+
# begin
|
|
17
|
+
# settings = Settings.new('theme' => 'dark')
|
|
18
|
+
# settings.save(file.path)
|
|
19
|
+
# Settings.load(file.path)['theme'] # => 'dark'
|
|
20
|
+
# ensure
|
|
21
|
+
# file.close!
|
|
22
|
+
# end
|
|
23
|
+
module Persistable
|
|
24
|
+
def self.included(base)
|
|
25
|
+
base.extend ClassMethods
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @example Add a YAML loader by including Persistable
|
|
29
|
+
# class Settings < Hash
|
|
30
|
+
# include Hashie::Extensions::MergeInitializer
|
|
31
|
+
# include Hashie::Extensions::Persistable
|
|
32
|
+
# end
|
|
33
|
+
# file = Tempfile.new(['hashie-example', '.yaml'], Dir.pwd)
|
|
34
|
+
# begin
|
|
35
|
+
# file.write("---\ntheme: dark\n")
|
|
36
|
+
# file.flush
|
|
37
|
+
# Settings.load(file.path) # => { 'theme' => 'dark' }
|
|
38
|
+
# ensure
|
|
39
|
+
# file.close!
|
|
40
|
+
# end
|
|
41
|
+
module ClassMethods
|
|
42
|
+
# @example Load safe YAML data and remember its path
|
|
43
|
+
# class Settings < Hash
|
|
44
|
+
# include Hashie::Extensions::MergeInitializer
|
|
45
|
+
# include Hashie::Extensions::Persistable
|
|
46
|
+
# end
|
|
47
|
+
# file = Tempfile.new(['hashie-example', '.yaml'], Dir.pwd)
|
|
48
|
+
# begin
|
|
49
|
+
# file.write("---\ntheme: dark\n")
|
|
50
|
+
# file.flush
|
|
51
|
+
# settings = Settings.load(file.path)
|
|
52
|
+
# settings['theme'] # => 'dark'
|
|
53
|
+
# settings.persistable_path # => File.expand_path(file.path)
|
|
54
|
+
# ensure
|
|
55
|
+
# file.close!
|
|
56
|
+
# end
|
|
57
|
+
def load(path)
|
|
58
|
+
path = File.expand_path(File.path(path))
|
|
59
|
+
data = Psych.safe_load(File.read(path), permitted_classes: [Symbol], aliases: false)
|
|
60
|
+
raise TypeError, 'persisted YAML must contain a hash' unless data.is_a?(::Hash)
|
|
61
|
+
|
|
62
|
+
new(data).tap { |object| object.instance_variable_set(:@persistable_path, path) }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @example Remember the last successful save location
|
|
67
|
+
# class Settings < Hash
|
|
68
|
+
# include Hashie::Extensions::MergeInitializer
|
|
69
|
+
# include Hashie::Extensions::Persistable
|
|
70
|
+
# end
|
|
71
|
+
# file = Tempfile.new(['hashie-example', '.yaml'], Dir.pwd)
|
|
72
|
+
# begin
|
|
73
|
+
# settings = Settings.new('theme' => 'dark')
|
|
74
|
+
# settings.persistable_path # => nil
|
|
75
|
+
# settings.save(file.path)
|
|
76
|
+
# settings.persistable_path # => File.expand_path(file.path)
|
|
77
|
+
# ensure
|
|
78
|
+
# file.close!
|
|
79
|
+
# end
|
|
80
|
+
attr_reader :persistable_path
|
|
81
|
+
|
|
82
|
+
# @example Save again without repeating the path
|
|
83
|
+
# class Settings < Hash
|
|
84
|
+
# include Hashie::Extensions::MergeInitializer
|
|
85
|
+
# include Hashie::Extensions::Persistable
|
|
86
|
+
# end
|
|
87
|
+
# file = Tempfile.new(['hashie-example', '.yaml'], Dir.pwd)
|
|
88
|
+
# begin
|
|
89
|
+
# settings = Settings.new('theme' => 'dark')
|
|
90
|
+
# settings.save(file.path)
|
|
91
|
+
# settings['theme'] = 'light'
|
|
92
|
+
# settings.save
|
|
93
|
+
# Settings.load(file.path)['theme'] # => 'light'
|
|
94
|
+
# ensure
|
|
95
|
+
# file.close!
|
|
96
|
+
# end
|
|
97
|
+
def save(path = persistable_path)
|
|
98
|
+
raise ArgumentError, 'a persistence path is required' if path.nil?
|
|
99
|
+
|
|
100
|
+
path = File.expand_path(File.path(path))
|
|
101
|
+
yaml = Psych.dump(persistable_data(self))
|
|
102
|
+
Tempfile.create(['.hashie-', '.yaml'], File.dirname(path)) do |file|
|
|
103
|
+
file.chmod(File.stat(path).mode & 0o777) if File.exist?(path)
|
|
104
|
+
file.write(yaml)
|
|
105
|
+
file.flush
|
|
106
|
+
file.fsync
|
|
107
|
+
file.close
|
|
108
|
+
File.rename(file.path, path)
|
|
109
|
+
end
|
|
110
|
+
@persistable_path = path
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def persistable_data(value, ancestors = [])
|
|
116
|
+
raise ArgumentError, 'cannot persist cyclic data' if ancestors.any? { |ancestor| ancestor.equal?(value) }
|
|
117
|
+
|
|
118
|
+
case value
|
|
119
|
+
when ::Hash
|
|
120
|
+
value.each_with_object({}) do |(key, child), data|
|
|
121
|
+
raise TypeError, 'persisted hash keys must be strings or symbols' unless key.is_a?(String) || key.is_a?(Symbol)
|
|
122
|
+
|
|
123
|
+
data[persistable_data(key)] = persistable_data(child, ancestors + [value])
|
|
124
|
+
end
|
|
125
|
+
when ::Array
|
|
126
|
+
value.map { |child| persistable_data(child, ancestors + [value]) }
|
|
127
|
+
when String
|
|
128
|
+
value.dup
|
|
129
|
+
when Symbol, Integer, Float, TrueClass, FalseClass, NilClass
|
|
130
|
+
value
|
|
131
|
+
else
|
|
132
|
+
raise TypeError, "cannot persist a value of class `#{value.class}`"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -1,11 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
5
|
+
# @example Inspect sorted hash entries
|
|
6
|
+
# class Settings < Hash
|
|
7
|
+
# include Hashie::Extensions::PrettyInspect
|
|
8
|
+
# end
|
|
9
|
+
# hash = Settings[:z, 2, :a, 1]
|
|
10
|
+
# hash.inspect # => "#<#{Settings} a=1 z=2>"
|
|
3
11
|
module PrettyInspect
|
|
4
12
|
def self.included(base)
|
|
5
13
|
base.send :alias_method, :hash_inspect, :inspect
|
|
6
14
|
base.send :alias_method, :inspect, :hashie_inspect
|
|
7
15
|
end
|
|
8
16
|
|
|
17
|
+
# @example Include the class and sorted key/value pairs
|
|
18
|
+
# class Settings < Hash
|
|
19
|
+
# include Hashie::Extensions::PrettyInspect
|
|
20
|
+
# end
|
|
21
|
+
# hash = Settings[:z, 2, :a, 1]
|
|
22
|
+
# hash.hashie_inspect # => "#<#{Settings} a=1 z=2>"
|
|
9
23
|
def hashie_inspect
|
|
10
24
|
ret = "#<#{self.class}"
|
|
11
25
|
keys.sort_by(&:to_s).each do |key|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# Copyright (c) Chad Fowler, Rich Kilmer, Jim Weirich and others.
|
|
2
4
|
# Portions copyright (c) Engine Yard and Andre Arko
|
|
3
5
|
#
|
|
@@ -14,22 +16,37 @@
|
|
|
14
16
|
|
|
15
17
|
module Hashie
|
|
16
18
|
module Extensions
|
|
19
|
+
# @example Compare versions numerically
|
|
20
|
+
# Hashie::Extensions::RubyVersion.new('3.10') > Hashie::Extensions::RubyVersion.new('3.9') # => true
|
|
17
21
|
class RubyVersion
|
|
18
22
|
include Comparable
|
|
19
23
|
|
|
24
|
+
# @example Inspect and replace parsed version segments
|
|
25
|
+
# version = Hashie::Extensions::RubyVersion.new('3.2.0.preview1')
|
|
26
|
+
# version.segments # => [3, 2, 0, 'preview', 1]
|
|
27
|
+
# version.segments = [3, 3, 0]
|
|
28
|
+
# version == Hashie::Extensions::RubyVersion.new('3.3') # => true
|
|
20
29
|
attr_accessor :segments
|
|
21
30
|
|
|
31
|
+
# @example Parse numeric and prerelease segments
|
|
32
|
+
# version = Hashie::Extensions::RubyVersion.new('3.2.0.rc1')
|
|
33
|
+
# version.segments # => [3, 2, 0, 'rc', 1]
|
|
22
34
|
def initialize(version)
|
|
23
35
|
@segments = split_to_segments(version)
|
|
24
36
|
end
|
|
25
37
|
|
|
38
|
+
# @example Ignore trailing zeros and order prereleases before releases
|
|
39
|
+
# version = Hashie::Extensions::RubyVersion
|
|
40
|
+
# version.new('3.2') <=> version.new('3.2.0') # => 0
|
|
41
|
+
# version.new('3.2.rc1') <=> version.new('3.2') # => -1
|
|
42
|
+
# version.new('3.10') <=> version.new('3.9') # => 1
|
|
26
43
|
def <=>(other)
|
|
27
44
|
lhsegments = segments
|
|
28
45
|
rhsegments = other.segments
|
|
29
46
|
|
|
30
47
|
lhsize = lhsegments.size
|
|
31
48
|
rhsize = rhsegments.size
|
|
32
|
-
limit =
|
|
49
|
+
limit = [lhsize, rhsize].max - 1
|
|
33
50
|
|
|
34
51
|
i = 0
|
|
35
52
|
|
|
@@ -1,17 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'hashie/extensions/ruby_version'
|
|
2
4
|
|
|
3
5
|
module Hashie
|
|
4
6
|
module Extensions
|
|
7
|
+
# @example Conditionally run code for the current Ruby
|
|
8
|
+
# klass = Class.new { include Hashie::Extensions::RubyVersionCheck }
|
|
9
|
+
# klass.with_minimum_ruby(RUBY_VERSION) { 'supported' } # => 'supported'
|
|
5
10
|
module RubyVersionCheck
|
|
6
11
|
def self.included(base)
|
|
7
12
|
base.extend ClassMethods
|
|
8
13
|
end
|
|
9
14
|
|
|
15
|
+
# @example Add version checks to a class
|
|
16
|
+
# klass = Class.new { include Hashie::Extensions::RubyVersionCheck }
|
|
17
|
+
# klass.with_minimum_ruby?('0.0.0') # => true
|
|
18
|
+
# klass.with_minimum_ruby?('999.0.0') # => false
|
|
10
19
|
module ClassMethods
|
|
20
|
+
# @example Run the block only when the minimum is satisfied
|
|
21
|
+
# klass = Class.new { include Hashie::Extensions::RubyVersionCheck }
|
|
22
|
+
# klass.with_minimum_ruby('0.0.0') { 'supported' } # => 'supported'
|
|
23
|
+
# klass.with_minimum_ruby('999.0.0') { raise 'not reached' } # => nil
|
|
11
24
|
def with_minimum_ruby(version)
|
|
12
25
|
yield if with_minimum_ruby?(version)
|
|
13
26
|
end
|
|
14
27
|
|
|
28
|
+
# @example Accept the current version as a minimum
|
|
29
|
+
# klass = Class.new { include Hashie::Extensions::RubyVersionCheck }
|
|
30
|
+
# klass.with_minimum_ruby?(RUBY_VERSION) # => true
|
|
31
|
+
# klass.with_minimum_ruby?('999.0.0') # => false
|
|
15
32
|
def with_minimum_ruby?(version)
|
|
16
33
|
RubyVersion.new(RUBY_VERSION) >= RubyVersion.new(version)
|
|
17
34
|
end
|