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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -0
  3. data/CONTRIBUTING.md +38 -1
  4. data/README.md +816 -181
  5. data/Rakefile +29 -1
  6. data/UPGRADING.md +18 -0
  7. data/hashie.gemspec +8 -7
  8. data/lib/hashie/array.rb +15 -12
  9. data/lib/hashie/clash.rb +59 -4
  10. data/lib/hashie/dash.rb +144 -16
  11. data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
  12. data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
  13. data/lib/hashie/extensions/block_access.rb +71 -0
  14. data/lib/hashie/extensions/coercion.rb +180 -19
  15. data/lib/hashie/extensions/dash/coercion.rb +34 -3
  16. data/lib/hashie/extensions/dash/indifferent_access.rb +83 -5
  17. data/lib/hashie/extensions/dash/lazy.rb +170 -0
  18. data/lib/hashie/extensions/dash/predefined_values.rb +69 -9
  19. data/lib/hashie/extensions/dash/property_translation.rb +134 -16
  20. data/lib/hashie/extensions/deep_fetch.rb +21 -9
  21. data/lib/hashie/extensions/deep_find.rb +20 -0
  22. data/lib/hashie/extensions/deep_grep.rb +69 -0
  23. data/lib/hashie/extensions/deep_locate.rb +15 -28
  24. data/lib/hashie/extensions/deep_merge.rb +16 -0
  25. data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
  26. data/lib/hashie/extensions/ignore_undeclared.rb +21 -3
  27. data/lib/hashie/extensions/indifferent_access.rb +100 -14
  28. data/lib/hashie/extensions/kernel_coercion.rb +58 -0
  29. data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
  30. data/lib/hashie/extensions/key_conversion.rb +9 -0
  31. data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
  32. data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
  33. data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
  34. data/lib/hashie/extensions/mash/required_keys.rb +111 -0
  35. data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
  36. data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
  37. data/lib/hashie/extensions/mash/symbolize_keys.rb +6 -2
  38. data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
  39. data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
  40. data/lib/hashie/extensions/merge_initializer.rb +9 -0
  41. data/lib/hashie/extensions/method_access.rb +93 -7
  42. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
  43. data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
  44. data/lib/hashie/extensions/persistable.rb +137 -0
  45. data/lib/hashie/extensions/pretty_inspect.rb +14 -0
  46. data/lib/hashie/extensions/ruby_version.rb +18 -1
  47. data/lib/hashie/extensions/ruby_version_check.rb +17 -0
  48. data/lib/hashie/extensions/strict_key_access.rb +59 -5
  49. data/lib/hashie/extensions/stringify_keys.rb +72 -17
  50. data/lib/hashie/extensions/symbolize_keys.rb +72 -16
  51. data/lib/hashie/hash.rb +16 -0
  52. data/lib/hashie/logger.rb +18 -1
  53. data/lib/hashie/mash.rb +293 -62
  54. data/lib/hashie/railtie.rb +27 -1
  55. data/lib/hashie/rash.rb +64 -4
  56. data/lib/hashie/trash.rb +8 -0
  57. data/lib/hashie/utils.rb +17 -6
  58. data/lib/hashie/version.rb +3 -1
  59. data/lib/hashie.rb +45 -0
  60. 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 # => NoMethodError
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? # => NoMethodError
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
- def initialize(hash = {})
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
- require 'yaml'
1
+ # frozen_string_literal: true
2
+
2
3
  require 'erb'
3
- require 'pathname'
4
+ require 'hashie/extensions/parsers/yaml_parser'
4
5
 
5
6
  module Hashie
6
7
  module Extensions
7
8
  module Parsers
8
- class YamlErbParser
9
- def initialize(file_path, options = {})
10
- @content = File.read(file_path)
11
- @file_path = file_path.is_a?(Pathname) ? file_path.to_s : file_path
12
- @options = options
13
- end
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
- permitted_classes = @options.fetch(:permitted_classes) { [] }
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 = (lhsize > rhsize ? lhsize : rhsize) - 1
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