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
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
|
-
|
|
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
|
-
|
|
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'
|
|
21
|
-
'changelog_uri'
|
|
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'
|
|
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 = '>=
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, '
|
|
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
|
-
# *
|
|
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
|
-
# *
|
|
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
|
-
# *
|
|
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
|
|
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, '
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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.
|
|
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? &&
|
|
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? &&
|
|
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
|
-
"
|
|
337
|
+
"the property `#{property}` " + message
|
|
224
338
|
end
|
|
225
339
|
|
|
226
340
|
def fail_no_property_error!(property)
|
|
227
|
-
raise NoMethodError,
|
|
341
|
+
raise NoMethodError,
|
|
342
|
+
"the property `#{property}` is not defined for `#{self.class.name || 'anonymous class'}`"
|
|
228
343
|
end
|
|
229
344
|
|
|
230
|
-
def
|
|
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(', ')
|