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
data/Rakefile CHANGED
@@ -12,10 +12,36 @@ RSpec::Core::RakeTask.new do |spec|
12
12
  spec.exclude_pattern = 'spec/integration/**/*_spec.rb'
13
13
  end
14
14
 
15
+ namespace :docs do
16
+ desc 'Validate executable YARD examples and public API documentation coverage'
17
+ task :examples do
18
+ ruby '-Ilib', 'script/test_documentation_examples.rb'
19
+ end
20
+
21
+ desc 'Validate every README Ruby example with the isolated documentation bundle'
22
+ task :readme do
23
+ Bundler.with_unbundled_env do
24
+ sh({ 'BUNDLE_GEMFILE' => 'spec/documentation/Gemfile' }, 'bundle', 'exec', 'ruby', '-Ilib', 'script/test_readme_examples.rb')
25
+ end
26
+ end
27
+ end
28
+
15
29
  require 'rubocop/rake_task'
16
30
  RuboCop::RakeTask.new(:rubocop)
31
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2')
32
+ RuboCop::RakeTask.new(:rubocop_exception_messages) do |task|
33
+ task.patterns = ['lib/**/*.rb', 'spec/**/*.rb']
34
+ task.options = [
35
+ '--only',
36
+ 'ExceptionMessages/Casing,ExceptionMessages/Punctuation,' \
37
+ 'ExceptionMessages/QuoteStyle,ExceptionMessages/RedundantExceptionName'
38
+ ]
39
+ end
40
+ Rake::Task[:rubocop].enhance([:rubocop_exception_messages])
41
+ end
17
42
 
18
43
  require_relative 'spec/support/integration_specs'
44
+ desc 'Install integration dependencies and run integration specs'
19
45
  task :integration_specs do
20
46
  next if ENV['CI']
21
47
 
@@ -32,4 +58,6 @@ task :integration_specs do
32
58
  end
33
59
  end
34
60
 
35
- task default: %i[rubocop spec integration_specs]
61
+ default_tasks = %i[spec integration_specs]
62
+ default_tasks.unshift(:rubocop) if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2')
63
+ task default: default_tasks
data/UPGRADING.md CHANGED
@@ -1,6 +1,24 @@
1
1
  Upgrading Hashie
2
2
  ================
3
3
 
4
+ ### Upgrading to 5.2.0
5
+
6
+ #### Safer YAML loading
7
+
8
+ `Hashie::Mash.load` continues to use `YamlErbParser` by default for compatibility, and that parser evaluates ERB. Do not load untrusted input with the default parser. To opt in to YAML parsing without ERB evaluation, pass `parser: Hashie::Extensions::Parsers::YamlParser`.
9
+
10
+ #### Ruby version
11
+
12
+ Hashie now requires Ruby 3.0 or newer. Ruby 2.7 is no longer supported; upgrade Ruby before updating Hashie.
13
+
14
+ #### Exception messages
15
+
16
+ Exception messages now begin with lowercase words, omit trailing periods, quote interpolated values
17
+ with backticks, and use consistent wording. Exception classes and the conditions under which
18
+ exceptions are raised have not changed.
19
+
20
+ Applications and tests that compare exact exception message strings must update their expectations.
21
+
4
22
  ### Upgrading to 5.0.0
5
23
 
6
24
  #### Mash initialization key conversion
data/hashie.gemspec CHANGED
@@ -1,4 +1,6 @@
1
- require File.expand_path('../lib/hashie/version', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path('lib/hashie/version', __dir__)
2
4
 
3
5
  Gem::Specification.new do |gem|
4
6
  gem.name = 'hashie'
@@ -17,16 +19,15 @@ Gem::Specification.new do |gem|
17
19
 
18
20
  if gem.respond_to?(:metadata)
19
21
  gem.metadata = {
20
- 'bug_tracker_uri' => 'https://github.com/hashie/hashie/issues',
21
- 'changelog_uri' => 'https://github.com/hashie/hashie/blob/master/CHANGELOG.md',
22
+ 'bug_tracker_uri' => 'https://github.com/hashie/hashie/issues',
23
+ 'changelog_uri' => 'https://github.com/hashie/hashie/blob/master/CHANGELOG.md',
22
24
  'documentation_uri' => 'https://www.rubydoc.info/gems/hashie',
23
- 'source_code_uri' => 'https://github.com/hashie/hashie'
25
+ 'source_code_uri' => 'https://github.com/hashie/hashie',
26
+ 'rubygems_mfa_required' => 'true'
24
27
  }
25
28
  end
26
29
 
27
- gem.required_ruby_version = '>= 2.7'
30
+ gem.required_ruby_version = '>= 3.0'
28
31
 
29
32
  gem.add_dependency 'logger' # for Ruby >= 3.4
30
-
31
- gem.add_development_dependency 'bundler'
32
33
  end
data/lib/hashie/array.rb CHANGED
@@ -1,21 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'hashie/extensions/array/pretty_inspect'
2
- require 'hashie/extensions/ruby_version_check'
3
4
 
4
5
  module Hashie
6
+ # An array with readable inspection and string-index digging.
7
+ #
8
+ # @example Build a Hashie array
9
+ # Hashie::Array[10, 20].dig('1') # => 20
5
10
  class Array < ::Array
6
11
  include Hashie::Extensions::Array::PrettyInspect
7
- include Hashie::Extensions::RubyVersionCheck
8
- with_minimum_ruby('2.3.0') do
9
- def dig(*indexes)
10
- converted_indexes = indexes.map do |idx|
11
- begin
12
- Integer(idx)
13
- rescue ArgumentError
14
- idx
15
- end
16
- end
17
- super(*converted_indexes)
12
+
13
+ # @example Accept string indexes when traversing nested arrays
14
+ # Hashie::Array[Hashie::Array['first', 'second']].dig('0', '1') # => 'second'
15
+ def dig(*indexes)
16
+ converted_indexes = indexes.map do |idx|
17
+ Integer(idx)
18
+ rescue ArgumentError
19
+ idx
18
20
  end
21
+ super(*converted_indexes)
19
22
  end
20
23
  end
21
24
  end
data/lib/hashie/clash.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'hashie/hash'
2
4
 
3
5
  module Hashie
@@ -21,14 +23,34 @@ module Hashie
21
23
  # all keys are converted to symbols since many libraries expect symbols explicitly
22
24
  # for keys.
23
25
  #
26
+ # @example Build an options hash through chained calls
27
+ # options = Hashie::Clash.new.conditions!.active(true)._end!.limit(10)
28
+ # options # => { conditions: { active: true }, limit: 10 }
24
29
  class Clash < ::Hash
30
+ # Raised when bang chaining encounters a value that is not a hash.
31
+ #
32
+ # @example Reject chaining into a scalar
33
+ # error = begin
34
+ # Hashie::Clash.new(limit: 10).limit!
35
+ # rescue Hashie::Clash::ChainError => exception
36
+ # exception
37
+ # end
38
+ # error.class # => Hashie::Clash::ChainError
39
+ # error.message # => 'tried to chain into a non-hash key'
25
40
  class ChainError < ::StandardError; end
26
41
  # The parent Clash if this Clash was created via chaining.
42
+ #
43
+ # @example Find the containing options hash
44
+ # root = Hashie::Clash.new
45
+ # root.conditions!._parent.equal?(root) # => true
27
46
  attr_reader :_parent
28
47
 
29
48
  # Initialize a new clash by passing in a Hash to
30
49
  # convert and, optionally, the parent to which this
31
50
  # Clash is chained.
51
+ #
52
+ # @example Normalize initial keys to symbols
53
+ # Hashie::Clash.new('limit' => 10) # => { limit: 10 }
32
54
  def initialize(other_hash = {}, parent = nil)
33
55
  @_parent = parent
34
56
  other_hash.each_pair do |k, v|
@@ -42,15 +64,40 @@ module Hashie
42
64
  # c = Hashie::Clash.new.foo('bar')
43
65
  # c.baz!.foo(123) # => c[:baz]
44
66
  # c.baz!._end! # => c
67
+ #
68
+ # @example Continue building the parent hash
69
+ # Hashie::Clash.new.conditions!.active(true)._end!.limit(10) # => { conditions: { active: true }, limit: 10 }
45
70
  def _end!
46
71
  _parent
47
72
  end
48
73
 
49
- def id(*args) #:nodoc:
74
+ # @example Serialize a Clash through YAML's encoding callback
75
+ # require 'yaml'
76
+ # options = Hashie::Clash.new.limit(10)
77
+ # YAML.unsafe_load(YAML.dump(options)) # => { limit: 10 }
78
+ def encode_with(coder)
79
+ coder['hash'] = to_h
80
+ coder['parent'] = _parent
81
+ end
82
+
83
+ # @example Restore a chained Clash through YAML's loading callback
84
+ # require 'yaml'
85
+ # root = Hashie::Clash.new.conditions!.active(true)._end!
86
+ # restored = YAML.unsafe_load(YAML.dump(root))
87
+ # restored[:conditions]._parent.equal?(restored) # => true
88
+ def init_with(coder)
89
+ initialize(coder['hash'], coder['parent'])
90
+ end
91
+
92
+ # @example Build an id option without colliding with object identity
93
+ # Hashie::Clash.new.id(42) # => { id: 42 }
94
+ def id(*args) # :nodoc:
50
95
  method_missing(:id, *args)
51
96
  end
52
97
 
53
- def merge_store(key, *args) #:nodoc:
98
+ # @example Merge repeated hash options through chained builder calls
99
+ # Hashie::Clash.new.conditions(active: true).conditions(role: 'admin') # => { conditions: { active: true, role: 'admin' } }
100
+ def merge_store(key, *args) # :nodoc:
54
101
  case args.length
55
102
  when 1
56
103
  val = args.first
@@ -63,7 +110,9 @@ module Hashie
63
110
  self
64
111
  end
65
112
 
66
- def method_missing(name, *args) #:nodoc:
113
+ # @example Build nested options with dynamic method names
114
+ # Hashie::Clash.new.conditions!.active(true)._end!.limit(10) # => { conditions: { active: true }, limit: 10 }
115
+ def method_missing(name, *args) # :nodoc:
67
116
  if args.empty? && name.to_s.end_with?('!')
68
117
  key = name[0...-1].to_sym
69
118
 
@@ -75,7 +124,7 @@ module Hashie
75
124
  when Hash
76
125
  self[key] = self.class.new(self[key], self)
77
126
  else
78
- raise ChainError, 'Tried to chain into a non-hash key.'
127
+ raise ChainError, 'tried to chain into a non-hash key'
79
128
  end
80
129
  elsif args.any?
81
130
  merge_store(name, *args)
@@ -84,8 +133,14 @@ module Hashie
84
133
  end
85
134
  end
86
135
 
136
+ # @example Discover supported builder calls
137
+ # options = Hashie::Clash.new(limit: 10)
138
+ # options.respond_to?(:conditions!) # => true
139
+ # options.respond_to?(:limit!) # => false
140
+ # options.respond_to?(:order) # => true
87
141
  def respond_to_missing?(method_name, _include_private = false)
88
142
  method_name = method_name.to_s
143
+ return super if %w[marshal_dump marshal_load _dump _load].include?(method_name)
89
144
 
90
145
  if method_name.end_with?('!')
91
146
  key = method_name[0...-1].to_sym
data/lib/hashie/dash.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'hashie/hash'
2
4
  require 'set'
3
5
 
@@ -12,27 +14,40 @@ module Hashie
12
14
  #
13
15
  # It is preferable to a Struct because of the in-class
14
16
  # API for defining properties as well as per-property defaults.
17
+ #
18
+ # @example Define a lightweight data object
19
+ # klass = Class.new(Hashie::Dash) { property :name, default: 'Ada' }
20
+ # klass.new.name # => 'Ada'
15
21
  class Dash < Hash
16
22
  include Hashie::Extensions::PrettyInspect
17
23
 
24
+ # @example Inspect declared properties
25
+ # klass = Class.new(Hashie::Dash) { property :name }
26
+ # klass.new(name: 'Ada').to_s.include?('name="Ada"') # => true
18
27
  alias to_s inspect
19
28
 
20
29
  # Defines a property on the Dash. Options are
21
30
  # as follows:
22
31
  #
23
- # * <tt>:default</tt> - Specify a default value for this property,
32
+ # * `:default` - Specify a default value for this property,
24
33
  # to be returned before a value is set on the property in a new
25
34
  # Dash.
26
35
  #
27
- # * <tt>:required</tt> - Specify the value as required for this
36
+ # * `:required` - Specify the value as required for this
28
37
  # property, to raise an error if a value is unset in a new or
29
38
  # existing Dash. If a Proc is provided, it will be run in the
30
39
  # context of the Dash instance. If a Symbol is provided, the
31
40
  # property it represents must not be nil. The property is only
32
41
  # required if the value is truthy.
33
42
  #
34
- # * <tt>:message</tt> - Specify custom error message for required property
43
+ # * `:message` - Specify custom error message for required property
35
44
  #
45
+ # @example Declare a required property and a default
46
+ # klass = Class.new(Hashie::Dash) do
47
+ # property :name, required: true
48
+ # property :active, default: true
49
+ # end
50
+ # klass.new(name: 'Ada').to_h # => { name: 'Ada', active: true }
36
51
  def self.property(property_name, options = {})
37
52
  properties << property_name
38
53
 
@@ -49,16 +64,32 @@ module Hashie
49
64
 
50
65
  condition = options.delete(:required)
51
66
  if condition
52
- message = options.delete(:message) || "is required for #{name}."
67
+ message = options.delete(:message) || "is required for `#{name}`"
53
68
  required_properties[property_name] = { condition: condition, message: message }
54
69
  elsif options.key?(:message)
55
- raise ArgumentError, 'The :message option should be used with :required option.'
70
+ raise ArgumentError, 'the :message option should be used with :required option'
56
71
  end
57
72
  end
58
73
 
59
74
  class << self
60
- attr_reader :properties, :defaults
75
+ # @example List the declared properties
76
+ # klass = Class.new(Hashie::Dash) { property :name }
77
+ # klass.properties.to_a # => [:name]
78
+ attr_reader :properties
79
+
80
+ # @example Read declared default values
81
+ # klass = Class.new(Hashie::Dash) { property :active, default: true }
82
+ # klass.defaults # => { active: true }
83
+ attr_reader :defaults
84
+
85
+ # @example List generated getter names
86
+ # klass = Class.new(Hashie::Dash) { property :name }
87
+ # klass.getters.to_a # => [:name]
61
88
  attr_reader :getters
89
+
90
+ # @example Inspect the requirement configured for a property
91
+ # klass = Class.new(Hashie::Dash) { property :name, required: true, message: 'must be supplied' }
92
+ # klass.required_properties # => { name: { condition: true, message: 'must be supplied' } }
62
93
  attr_reader :required_properties
63
94
  end
64
95
  instance_variable_set('@properties', Set.new)
@@ -66,6 +97,11 @@ module Hashie
66
97
  instance_variable_set('@defaults', {})
67
98
  instance_variable_set('@required_properties', {})
68
99
 
100
+ # @example Subclasses inherit independent property definitions
101
+ # parent = Class.new(Hashie::Dash) { property :name, default: 'Ada' }
102
+ # child = Class.new(parent) { property :active, default: true }
103
+ # child.new.to_h # => { name: 'Ada', active: true }
104
+ # parent.properties.to_a # => [:name]
69
105
  def self.inherited(klass)
70
106
  super
71
107
  (@subclasses ||= Set.new) << klass
@@ -77,30 +113,49 @@ module Hashie
77
113
 
78
114
  # Check to see if the specified property has already been
79
115
  # defined.
116
+ #
117
+ # @example Check declared and undeclared properties
118
+ # klass = Class.new(Hashie::Dash) { property :name }
119
+ # klass.property?(:name) # => true
120
+ # klass.property?(:missing) # => false
80
121
  def self.property?(name)
81
122
  properties.include? name
82
123
  end
83
124
 
84
125
  # Check to see if the specified property is
85
126
  # required.
127
+ #
128
+ # @example Identify a required declaration
129
+ # klass = Class.new(Hashie::Dash) { property :name, required: true }
130
+ # klass.required?(:name) # => true
131
+ # klass.required?(:missing) # => false
86
132
  def self.required?(name)
87
133
  required_properties.key? name
88
134
  end
89
135
 
90
136
  private_class_method def self.define_getter_for(property_name)
91
137
  return if getters.include?(property_name)
138
+
92
139
  define_method(property_name) { |&block| self.[](property_name, &block) }
93
140
  getters << property_name
94
141
  end
95
142
 
96
143
  private_class_method def self.define_setter_for(property_name)
97
144
  setter = :"#{property_name}="
98
- return if instance_methods.include?(setter)
145
+ return if method_defined?(setter)
146
+
99
147
  define_method(setter) { |value| self.[]=(property_name, value) }
100
148
  end
101
149
 
102
150
  # You may initialize a Dash with an attributes hash
103
151
  # just like you would many other kinds of data objects.
152
+ #
153
+ # @example Initialize declared attributes and defaults
154
+ # klass = Class.new(Hashie::Dash) do
155
+ # property :name
156
+ # property :active, default: true
157
+ # end
158
+ # klass.new(name: 'Ada').to_h # => { name: 'Ada', active: true }
104
159
  def initialize(attributes = {}, &block)
105
160
  super(&block)
106
161
 
@@ -114,9 +169,13 @@ module Hashie
114
169
 
115
170
  # Retrieve a value from the Dash (will return the
116
171
  # property's default value if it hasn't been set).
172
+ #
173
+ # @example Read a declared value by key
174
+ # klass = Class.new(Hashie::Dash) { property :name, default: 'Ada' }
175
+ # klass.new[:name] # => 'Ada'
117
176
  def [](property)
118
177
  assert_property_exists! property
119
- value = super(property)
178
+ value = super
120
179
  # If the value is a lambda, proc, or whatever answers to call, eval the thing!
121
180
  if value.is_a? Proc
122
181
  self[property] = value.call # Set the result of the call as a value
@@ -128,12 +187,23 @@ module Hashie
128
187
 
129
188
  # Set a value on the Dash in a Hash-like way. Only works
130
189
  # on pre-existing properties.
190
+ #
191
+ # @example Assign a declared property
192
+ # klass = Class.new(Hashie::Dash) { property :name }
193
+ # dash = klass.new
194
+ # dash[:name] = 'Ada'
195
+ # dash.name # => 'Ada'
131
196
  def []=(property, value)
132
197
  assert_property_required! property, value
133
198
  assert_property_exists! property
134
- super(property, value)
199
+ super
135
200
  end
136
201
 
202
+ # @example Merge into a copy with conflict resolution
203
+ # klass = Class.new(Hashie::Dash) { property :count }
204
+ # dash = klass.new(count: 2)
205
+ # dash.merge(count: 3) { |_key, old, new| old + new }.to_h # => { count: 5 }
206
+ # dash.count # => 2
137
207
  def merge(other_hash)
138
208
  new_dash = dup
139
209
  other_hash.each do |k, v|
@@ -142,6 +212,11 @@ module Hashie
142
212
  new_dash
143
213
  end
144
214
 
215
+ # @example Merge declared values in place
216
+ # klass = Class.new(Hashie::Dash) { property :name }
217
+ # dash = klass.new(name: 'Ada')
218
+ # dash.merge!(name: 'Grace')
219
+ # dash.name # => 'Grace'
145
220
  def merge!(other_hash)
146
221
  other_hash.each do |k, v|
147
222
  self[k] = block_given? ? yield(k, self[k], v) : v
@@ -149,6 +224,13 @@ module Hashie
149
224
  self
150
225
  end
151
226
 
227
+ # @example Restore defaults for omitted attributes
228
+ # klass = Class.new(Hashie::Dash) do
229
+ # property :name
230
+ # property :active, default: true
231
+ # end
232
+ # dash = klass.new(name: 'Ada', active: false)
233
+ # dash.replace(name: 'Grace').to_h # => { name: 'Grace', active: true }
152
234
  def replace(other_hash)
153
235
  other_hash = self.class.defaults.merge(other_hash)
154
236
  (keys - other_hash.keys).each { |key| delete(key) }
@@ -156,18 +238,49 @@ module Hashie
156
238
  self
157
239
  end
158
240
 
241
+ # @example Include declared but unset properties in a plain hash
242
+ # klass = Class.new(Hashie::Dash) { property :name }
243
+ # klass.new.to_h # => { name: nil }
244
+ # klass.new.to_h.class # => Hash
159
245
  def to_h
160
- defaults = ::Hash[self.class.properties.map { |prop| [prop, self.class.defaults[prop]] }]
246
+ defaults = self.class.properties.to_h { |prop| [prop, self.class.defaults[prop]] }
161
247
 
162
248
  defaults.merge(self)
163
249
  end
250
+ # @example Convert a Dash to a plain hash
251
+ # klass = Class.new(Hashie::Dash) { property :name }
252
+ # klass.new(name: 'Ada').to_hash # => { name: 'Ada' }
164
253
  alias to_hash to_h
165
254
 
255
+ # ActiveSupport's Hash#deep_transform_keys builds up its result via
256
+ # `self.class.new`, expecting to be able to start from an empty hash
257
+ # and fill in keys one at a time. Since Dash enforces required
258
+ # properties at initialization and restricts which keys may be set,
259
+ # this breaks for Dash (and Trash) - see
260
+ # https://github.com/hashie/hashie/issues/559. Deep transforming a
261
+ # Dash's keys no longer produces a valid Dash anyway (the resulting
262
+ # keys may not correspond to any defined property), so fall back to
263
+ # operating on a plain Hash instead.
264
+ #
265
+ # @example Transform nested keys without Dash's property restrictions
266
+ # klass = Class.new(Hashie::Dash) { property :user }
267
+ # dash = klass.new(user: { name: 'Ada' })
268
+ # dash.deep_transform_keys(&:to_s) # => { 'user' => { 'name' => 'Ada' } }
269
+ def deep_transform_keys(&block)
270
+ _deep_transform_keys_in_object(to_h, &block)
271
+ end
272
+
273
+ # @example Update attributes and reapply defaults for nil values
274
+ # klass = Class.new(Hashie::Dash) { property :name, default: 'Ada' }
275
+ # dash = klass.new(name: 'Grace')
276
+ # dash.update_attributes!(name: nil)
277
+ # dash.name # => 'Ada'
166
278
  def update_attributes!(attributes)
167
279
  update_attributes(attributes)
168
280
 
169
281
  self.class.defaults.each_pair do |prop, value|
170
282
  next unless fetch(prop, nil).nil?
283
+
171
284
  self[prop] = begin
172
285
  val = value.dup
173
286
  if val.is_a?(Proc)
@@ -188,7 +301,7 @@ module Hashie
188
301
  def initialize_attributes(attributes)
189
302
  return unless attributes
190
303
 
191
- cleaned_attributes = attributes.reject { |_attr, value| value.nil? }
304
+ cleaned_attributes = attributes.compact
192
305
  update_attributes!(cleaned_attributes)
193
306
  end
194
307
 
@@ -211,23 +324,25 @@ module Hashie
211
324
  end
212
325
 
213
326
  def assert_property_set!(property)
214
- fail_property_required_error!(property) if send(property).nil? && required?(property)
327
+ fail_property_required_error!(property) if send(property).nil? && _required?(property)
215
328
  end
216
329
 
217
330
  def assert_property_required!(property, value)
218
- fail_property_required_error!(property) if value.nil? && required?(property)
331
+ fail_property_required_error!(property) if value.nil? && _required?(property)
219
332
  end
220
333
 
221
334
  def fail_property_required_error!(property)
335
+ message = self.class.required_properties[property][:message]
222
336
  raise ArgumentError,
223
- "The property '#{property}' #{self.class.required_properties[property][:message]}"
337
+ "the property `#{property}` " + message
224
338
  end
225
339
 
226
340
  def fail_no_property_error!(property)
227
- raise NoMethodError, "The property '#{property}' is not defined for #{self.class.name}."
341
+ raise NoMethodError,
342
+ "the property `#{property}` is not defined for `#{self.class.name || 'anonymous class'}`"
228
343
  end
229
344
 
230
- def required?(property)
345
+ def _required?(property)
231
346
  return false unless self.class.required?(property)
232
347
 
233
348
  condition = self.class.required_properties[property][:condition]
@@ -237,5 +352,18 @@ module Hashie
237
352
  else !!condition
238
353
  end
239
354
  end
355
+
356
+ def _deep_transform_keys_in_object(object, &block)
357
+ case object
358
+ when ::Hash
359
+ object.each_with_object({}) do |(key, value), result|
360
+ result[yield(key)] = _deep_transform_keys_in_object(value, &block)
361
+ end
362
+ when ::Array
363
+ object.map { |e| _deep_transform_keys_in_object(e, &block) }
364
+ else
365
+ object
366
+ end
367
+ end
240
368
  end
241
369
  end
@@ -1,8 +1,46 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hashie
2
4
  module Extensions
5
+ # Compatibility extensions for Active Support.
6
+ #
7
+ # @example
8
+ # require 'active_support/core_ext/hash/except'
9
+ # require 'hashie/extensions/active_support/core_ext/hash'
10
+ # klass = Class.new(Hashie::Mash) do
11
+ # include Hashie::Extensions::ActiveSupport::CoreExt::Hash
12
+ # end
13
+ # klass.new(name: 'Ada', age: 37).except(:age).to_h # => {'name' => 'Ada'}
3
14
  module ActiveSupport
15
+ # Adapts Active Support core extensions for Hashie objects.
16
+ #
17
+ # @example
18
+ # require 'active_support/core_ext/hash/except'
19
+ # require 'hashie/extensions/active_support/core_ext/hash'
20
+ # klass = Class.new(Hashie::Mash) do
21
+ # include Hashie::Extensions::ActiveSupport::CoreExt::Hash
22
+ # end
23
+ # klass.new(name: 'Ada').except(:name).to_h # => {}
4
24
  module CoreExt
25
+ # Converts excluded keys using Mash's key normalization.
26
+ #
27
+ # @example
28
+ # require 'active_support/core_ext/hash/except'
29
+ # require 'hashie/extensions/active_support/core_ext/hash'
30
+ # klass = Class.new(Hashie::Mash) do
31
+ # include Hashie::Extensions::ActiveSupport::CoreExt::Hash
32
+ # end
33
+ # klass.new(name: 'Ada', age: 37).except(:age).to_h # => {'name' => 'Ada'}
5
34
  module Hash
35
+ # @example Exclude symbol keys from a string-keyed Mash
36
+ # require 'active_support/core_ext/hash/except'
37
+ # require 'hashie/extensions/active_support/core_ext/hash'
38
+ # klass = Class.new(Hashie::Mash) do
39
+ # include Hashie::Extensions::ActiveSupport::CoreExt::Hash
40
+ # end
41
+ # mash = klass.new(name: 'Ada', age: 37)
42
+ # mash.except(:age).to_h # => {'name' => 'Ada'}
43
+ # mash.keys # => ['name', 'age']
6
44
  def except(*keys)
7
45
  string_keys = keys.map { |key| convert_key(key) }
8
46
  super(*string_keys)
@@ -1,12 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hashie
2
4
  module Extensions
3
5
  module Array
6
+ # Formats array inspection with the concrete class name.
7
+ #
8
+ # @example
9
+ # klass = Class.new(Array) do
10
+ # include Hashie::Extensions::Array::PrettyInspect
11
+ # end
12
+ # array = klass.new([1, 'two'])
13
+ # array.inspect # => "#<#{klass} [1, \"two\"]>"
4
14
  module PrettyInspect
15
+ # @example Retain the original inspection as array_inspect
16
+ # klass = Class.new(Array) do
17
+ # include Hashie::Extensions::Array::PrettyInspect
18
+ # end
19
+ # klass.new([1, 2]).array_inspect # => '[1, 2]'
5
20
  def self.included(base)
6
21
  base.send :alias_method, :array_inspect, :inspect
7
22
  base.send :alias_method, :inspect, :hashie_inspect
8
23
  end
9
24
 
25
+ # @example
26
+ # klass = Class.new(Array) do
27
+ # include Hashie::Extensions::Array::PrettyInspect
28
+ # end
29
+ # klass.new([1, 2]).hashie_inspect # => "#<#{klass} [1, 2]>"
10
30
  def hashie_inspect
11
31
  ret = "#<#{self.class} ["
12
32
  ret << to_a.map(&:inspect).join(', ')