hashie 3.6.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +95 -7
- data/UPGRADING.md +78 -2
- data/hashie.gemspec +2 -1
- data/lib/hashie.rb +20 -19
- data/lib/hashie/dash.rb +2 -1
- data/lib/hashie/extensions/active_support/core_ext/hash.rb +14 -0
- data/lib/hashie/extensions/coercion.rb +23 -16
- data/lib/hashie/extensions/dash/indifferent_access.rb +20 -1
- data/lib/hashie/extensions/dash/property_translation.rb +5 -2
- data/lib/hashie/extensions/deep_fetch.rb +4 -2
- data/lib/hashie/extensions/deep_find.rb +12 -3
- data/lib/hashie/extensions/deep_locate.rb +22 -7
- data/lib/hashie/extensions/indifferent_access.rb +1 -3
- data/lib/hashie/extensions/key_conflict_warning.rb +55 -0
- data/lib/hashie/extensions/mash/define_accessors.rb +90 -0
- data/lib/hashie/extensions/mash/keep_original_keys.rb +2 -1
- data/lib/hashie/extensions/mash/safe_assignment.rb +3 -1
- data/lib/hashie/extensions/method_access.rb +5 -2
- data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +9 -4
- data/lib/hashie/extensions/strict_key_access.rb +8 -4
- data/lib/hashie/hash.rb +16 -9
- data/lib/hashie/mash.rb +99 -43
- data/lib/hashie/railtie.rb +7 -0
- data/lib/hashie/rash.rb +1 -1
- data/lib/hashie/version.rb +1 -1
- data/spec/hashie/dash_spec.rb +18 -8
- data/spec/hashie/extensions/coercion_spec.rb +17 -8
- data/spec/hashie/extensions/deep_find_spec.rb +12 -6
- data/spec/hashie/extensions/deep_locate_spec.rb +2 -1
- data/spec/hashie/extensions/deep_merge_spec.rb +6 -2
- data/spec/hashie/extensions/ignore_undeclared_spec.rb +2 -1
- data/spec/hashie/extensions/mash/define_accessors_spec.rb +90 -0
- data/spec/hashie/extensions/method_access_spec.rb +8 -1
- data/spec/hashie/extensions/strict_key_access_spec.rb +9 -10
- data/spec/hashie/extensions/symbolize_keys_spec.rb +3 -1
- data/spec/hashie/hash_spec.rb +45 -6
- data/spec/hashie/mash_spec.rb +314 -8
- data/spec/hashie/trash_spec.rb +9 -3
- data/spec/integration/elasticsearch/integration_spec.rb +3 -2
- data/spec/integration/rails/app.rb +5 -12
- data/spec/integration/rails/integration_spec.rb +22 -1
- metadata +8 -4
@@ -7,11 +7,23 @@ module Hashie
|
|
7
7
|
base.send :include, Hashie::Extensions::IndifferentAccess
|
8
8
|
end
|
9
9
|
|
10
|
+
def self.maybe_extend(base)
|
11
|
+
return unless requires_class_methods?(base)
|
12
|
+
|
13
|
+
base.extend(ClassMethods)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.requires_class_methods?(klass)
|
17
|
+
klass <= Hashie::Dash &&
|
18
|
+
!klass.singleton_class.included_modules.include?(ClassMethods)
|
19
|
+
end
|
20
|
+
private_class_method :requires_class_methods?
|
21
|
+
|
10
22
|
module ClassMethods
|
11
23
|
# Check to see if the specified property has already been
|
12
24
|
# defined.
|
13
25
|
def property?(name)
|
14
|
-
name = translations[name.to_sym] if
|
26
|
+
name = translations[name.to_sym] if translation_for?(name)
|
15
27
|
name = name.to_s
|
16
28
|
!!properties.find { |property| property.to_s == name }
|
17
29
|
end
|
@@ -30,6 +42,13 @@ module Hashie
|
|
30
42
|
name = name.to_s
|
31
43
|
!!transforms.keys.find { |key| key.to_s == name }
|
32
44
|
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def translation_for?(name)
|
49
|
+
included_modules.include?(Hashie::Extensions::Dash::PropertyTranslation) &&
|
50
|
+
translation_exists?(name)
|
51
|
+
end
|
33
52
|
end
|
34
53
|
end
|
35
54
|
end
|
@@ -58,7 +58,9 @@ module Hashie
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def permitted_input_keys
|
61
|
-
@permitted_input_keys ||=
|
61
|
+
@permitted_input_keys ||=
|
62
|
+
properties
|
63
|
+
.map { |property| inverse_translations.fetch property, property }
|
62
64
|
end
|
63
65
|
|
64
66
|
# Defines a property on the Trash. Options are as follows:
|
@@ -135,7 +137,8 @@ module Hashie
|
|
135
137
|
end
|
136
138
|
|
137
139
|
def fail_self_transformation_error!(property_name)
|
138
|
-
raise ArgumentError,
|
140
|
+
raise ArgumentError,
|
141
|
+
"Property name (#{property_name}) and :from option must not be the same"
|
139
142
|
end
|
140
143
|
|
141
144
|
def valid_transformer?(transformer)
|
@@ -9,7 +9,8 @@ module Hashie
|
|
9
9
|
#
|
10
10
|
# options.deep_fetch(:user, :non_existent_key) { 'a value' } #=> 'a value'
|
11
11
|
#
|
12
|
-
# This is particularly useful for fetching values from deeply nested api responses
|
12
|
+
# This is particularly useful for fetching values from deeply nested api responses
|
13
|
+
# or params hashes.
|
13
14
|
module DeepFetch
|
14
15
|
class UndefinedPathError < StandardError; end
|
15
16
|
|
@@ -20,7 +21,8 @@ module Hashie
|
|
20
21
|
obj.fetch(arg)
|
21
22
|
rescue ArgumentError, IndexError, NoMethodError => e
|
22
23
|
break yield(arg) if block
|
23
|
-
raise UndefinedPathError,
|
24
|
+
raise UndefinedPathError,
|
25
|
+
"Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace
|
24
26
|
end
|
25
27
|
end
|
26
28
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'hashie/extensions/deep_locate'
|
1
2
|
module Hashie
|
2
3
|
module Extensions
|
3
4
|
module DeepFind
|
@@ -24,7 +25,12 @@ module Hashie
|
|
24
25
|
# Performs a depth-first search on deeply nested data structures for
|
25
26
|
# a key and returns all occurrences of the key.
|
26
27
|
#
|
27
|
-
# options = {
|
28
|
+
# options = {
|
29
|
+
# users: [
|
30
|
+
# { location: {address: '123 Street'} },
|
31
|
+
# { location: {address: '234 Street'}}
|
32
|
+
# ]
|
33
|
+
# }
|
28
34
|
# options.extend(Hashie::Extensions::DeepFind)
|
29
35
|
# options.deep_find_all(:address) # => ['123 Street', '234 Street']
|
30
36
|
#
|
@@ -33,7 +39,10 @@ module Hashie
|
|
33
39
|
# end
|
34
40
|
#
|
35
41
|
# my_hash = MyHash.new
|
36
|
-
# my_hash[:users] = [
|
42
|
+
# my_hash[:users] = [
|
43
|
+
# {location: {address: '123 Street'}},
|
44
|
+
# {location: {address: '234 Street'}}
|
45
|
+
# ]
|
37
46
|
# my_hash.deep_find_all(:address) # => ['123 Street', '234 Street']
|
38
47
|
def deep_find_all(key)
|
39
48
|
matches = _deep_find_all(key)
|
@@ -49,7 +58,7 @@ module Hashie
|
|
49
58
|
end
|
50
59
|
|
51
60
|
def _deep_find_all(key, object = self, matches = [])
|
52
|
-
deep_locate_result =
|
61
|
+
deep_locate_result = DeepLocate.deep_locate(key, object).tap do |result|
|
53
62
|
result.map! { |element| element[key] }
|
54
63
|
end
|
55
64
|
|
@@ -14,10 +14,12 @@ module Hashie
|
|
14
14
|
# ...
|
15
15
|
# ]
|
16
16
|
#
|
17
|
-
#
|
17
|
+
# DeepLocate.deep_locate -> (key, value, object) { key == :title }, books
|
18
18
|
# # => [{:title=>"Ruby for beginners", :pages=>120}, ...]
|
19
19
|
def self.deep_locate(comparator, object)
|
20
|
-
|
20
|
+
unless comparator.respond_to?(:call)
|
21
|
+
comparator = _construct_key_comparator(comparator, object)
|
22
|
+
end
|
21
23
|
|
22
24
|
_deep_locate(comparator, object)
|
23
25
|
end
|
@@ -53,17 +55,21 @@ module Hashie
|
|
53
55
|
# # http://ruby-journal.com/becareful-with-space-in-lambda-hash-rocket-syntax-between-ruby-1-dot-9-and-2-dot-0/
|
54
56
|
#
|
55
57
|
# books.deep_locate -> (key, value, object) { key == :title && value.include?("Ruby") }
|
56
|
-
# # => [{:title=>"Ruby for beginners", :pages=>120},
|
58
|
+
# # => [{:title=>"Ruby for beginners", :pages=>120},
|
59
|
+
# # {:title=>"Ruby for the rest of us", :pages=>576}]
|
57
60
|
#
|
58
61
|
# books.deep_locate -> (key, value, object) { key == :pages && value <= 120 }
|
59
|
-
# # => [{:title=>"Ruby for beginners", :pages=>120},
|
62
|
+
# # => [{:title=>"Ruby for beginners", :pages=>120},
|
63
|
+
# # {:title=>"CSS for intermediates", :pages=>80}]
|
60
64
|
def deep_locate(comparator)
|
61
65
|
Hashie::Extensions::DeepLocate.deep_locate(comparator, self)
|
62
66
|
end
|
63
67
|
|
64
68
|
def self._construct_key_comparator(search_key, object)
|
65
|
-
|
66
|
-
|
69
|
+
if object.respond_to?(:indifferent_access?) && object.indifferent_access? ||
|
70
|
+
activesupport_indifferent?(object)
|
71
|
+
search_key = search_key.to_s
|
72
|
+
end
|
67
73
|
|
68
74
|
lambda do |non_callable_object|
|
69
75
|
->(key, _, _) { key == non_callable_object }
|
@@ -73,7 +79,10 @@ module Hashie
|
|
73
79
|
|
74
80
|
def self._deep_locate(comparator, object, result = [])
|
75
81
|
if object.is_a?(::Enumerable)
|
76
|
-
|
82
|
+
if object.any? { |value| _match_comparator?(value, comparator, object) }
|
83
|
+
result.push object
|
84
|
+
end
|
85
|
+
|
77
86
|
(object.respond_to?(:values) ? object.values : object.entries).each do |value|
|
78
87
|
_deep_locate(comparator, value, result)
|
79
88
|
end
|
@@ -93,6 +102,12 @@ module Hashie
|
|
93
102
|
comparator.call(key, value, object)
|
94
103
|
end
|
95
104
|
private_class_method :_match_comparator?
|
105
|
+
|
106
|
+
def self.activesupport_indifferent?(object)
|
107
|
+
defined?(::ActiveSupport::HashWithIndifferentAccess) &&
|
108
|
+
object.is_a?(::ActiveSupport::HashWithIndifferentAccess)
|
109
|
+
end
|
110
|
+
private_class_method :activesupport_indifferent?
|
96
111
|
end
|
97
112
|
end
|
98
113
|
end
|
@@ -24,9 +24,7 @@ module Hashie
|
|
24
24
|
#
|
25
25
|
module IndifferentAccess
|
26
26
|
def self.included(base)
|
27
|
-
Hashie::Extensions::Dash::IndifferentAccess
|
28
|
-
base.extend(extension) if base <= Hashie::Dash && !base.singleton_class.included_modules.include?(extension)
|
29
|
-
end
|
27
|
+
Hashie::Extensions::Dash::IndifferentAccess.maybe_extend(base)
|
30
28
|
|
31
29
|
base.class_eval do
|
32
30
|
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Hashie
|
2
|
+
module Extensions
|
3
|
+
module KeyConflictWarning
|
4
|
+
class CannotDisableMashWarnings < StandardError
|
5
|
+
def initialize
|
6
|
+
super(
|
7
|
+
'You cannot disable warnings on the base Mash class. ' \
|
8
|
+
'Please subclass the Mash and disable it in the subclass.'
|
9
|
+
)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Disable the logging of warnings based on keys conflicting keys/methods
|
14
|
+
#
|
15
|
+
# @api semipublic
|
16
|
+
# @return [void]
|
17
|
+
def disable_warnings(*method_keys)
|
18
|
+
raise CannotDisableMashWarnings if self == Hashie::Mash
|
19
|
+
if method_keys.any?
|
20
|
+
disabled_warnings.concat(method_keys).tap(&:flatten!).uniq!
|
21
|
+
else
|
22
|
+
disabled_warnings.clear
|
23
|
+
end
|
24
|
+
|
25
|
+
@disable_warnings = true
|
26
|
+
end
|
27
|
+
|
28
|
+
# Checks whether this class disables warnings for conflicting keys/methods
|
29
|
+
#
|
30
|
+
# @api semipublic
|
31
|
+
# @return [Boolean]
|
32
|
+
def disable_warnings?(method_key = nil)
|
33
|
+
return disabled_warnings.include?(method_key) if disabled_warnings.any? && method_key
|
34
|
+
@disable_warnings ||= false
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns an array of methods that this class disables warnings for.
|
38
|
+
#
|
39
|
+
# @api semipublic
|
40
|
+
# @return [Boolean]
|
41
|
+
def disabled_warnings
|
42
|
+
@_disabled_warnings ||= []
|
43
|
+
end
|
44
|
+
|
45
|
+
# Inheritance hook that sets class configuration when inherited.
|
46
|
+
#
|
47
|
+
# @api semipublic
|
48
|
+
# @return [void]
|
49
|
+
def inherited(subclass)
|
50
|
+
super
|
51
|
+
subclass.disable_warnings(disabled_warnings) if disable_warnings?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Hashie
|
2
|
+
module Extensions
|
3
|
+
module Mash
|
4
|
+
module DefineAccessors
|
5
|
+
def self.included(klass)
|
6
|
+
klass.class_eval do
|
7
|
+
mod = Ext.new
|
8
|
+
include mod
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.extended(obj)
|
13
|
+
included(obj.singleton_class)
|
14
|
+
end
|
15
|
+
|
16
|
+
class Ext < Module
|
17
|
+
def initialize
|
18
|
+
mod = self
|
19
|
+
define_method(:method_missing) do |method_name, *args, &block|
|
20
|
+
key, suffix = method_name_and_suffix(method_name)
|
21
|
+
case suffix
|
22
|
+
when '='.freeze
|
23
|
+
mod.define_writer(key, method_name)
|
24
|
+
when '?'.freeze
|
25
|
+
mod.define_predicate(key, method_name)
|
26
|
+
when '!'.freeze
|
27
|
+
mod.define_initializing_reader(key, method_name)
|
28
|
+
when '_'.freeze
|
29
|
+
mod.define_underbang_reader(key, method_name)
|
30
|
+
else
|
31
|
+
mod.define_reader(key, method_name)
|
32
|
+
end
|
33
|
+
send(method_name, *args, &block)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def define_reader(key, method_name)
|
38
|
+
define_method(method_name) do |&block|
|
39
|
+
if key? method_name
|
40
|
+
self.[](method_name, &block)
|
41
|
+
else
|
42
|
+
self.[](key, &block)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def define_writer(key, method_name)
|
48
|
+
define_method(method_name) do |value = nil|
|
49
|
+
if key? method_name
|
50
|
+
self.[](method_name, &proc)
|
51
|
+
else
|
52
|
+
assign_property(key, value)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def define_predicate(key, method_name)
|
58
|
+
define_method(method_name) do
|
59
|
+
if key? method_name
|
60
|
+
self.[](method_name, &proc)
|
61
|
+
else
|
62
|
+
!!self[key]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def define_initializing_reader(key, method_name)
|
68
|
+
define_method(method_name) do
|
69
|
+
if key? method_name
|
70
|
+
self.[](method_name, &proc)
|
71
|
+
else
|
72
|
+
initializing_reader(key)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def define_underbang_reader(key, method_name)
|
78
|
+
define_method(method_name) do
|
79
|
+
if key? method_name
|
80
|
+
self.[](key, &proc)
|
81
|
+
else
|
82
|
+
underbang_reader(key)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -15,7 +15,8 @@ module Hashie
|
|
15
15
|
# mash[:symbol_key] == mash['symbol_key'] #=> true
|
16
16
|
module KeepOriginalKeys
|
17
17
|
def self.included(descendant)
|
18
|
-
|
18
|
+
error_message = "#{descendant} is not a kind of Hashie::Mash"
|
19
|
+
raise ArgumentError, error_message unless descendant <= Hashie::Mash
|
19
20
|
end
|
20
21
|
|
21
22
|
private
|
@@ -3,7 +3,9 @@ module Hashie
|
|
3
3
|
module Mash
|
4
4
|
module SafeAssignment
|
5
5
|
def custom_writer(key, *args) #:nodoc:
|
6
|
-
|
6
|
+
if !key?(key) && respond_to?(key, true)
|
7
|
+
raise ArgumentError, "The property #{key} clashes with an existing method."
|
8
|
+
end
|
7
9
|
super
|
8
10
|
end
|
9
11
|
|
@@ -73,7 +73,9 @@ module Hashie
|
|
73
73
|
end
|
74
74
|
|
75
75
|
def method_missing(name, *args)
|
76
|
-
|
76
|
+
if args.size == 1 && name.to_s =~ /(.*)=$/
|
77
|
+
return self[convert_key(Regexp.last_match[1])] = args.first
|
78
|
+
end
|
77
79
|
|
78
80
|
super
|
79
81
|
end
|
@@ -231,7 +233,8 @@ module Hashie
|
|
231
233
|
# underscores.
|
232
234
|
module MethodAccessWithOverride
|
233
235
|
def self.included(base)
|
234
|
-
[MethodReader, MethodOverridingWriter,
|
236
|
+
[MethodReader, MethodOverridingWriter,
|
237
|
+
MethodQuery, MethodOverridingInitializer].each do |mod|
|
235
238
|
base.send :include, mod
|
236
239
|
end
|
237
240
|
end
|
@@ -6,19 +6,24 @@ module Hashie
|
|
6
6
|
module Extensions
|
7
7
|
module Parsers
|
8
8
|
class YamlErbParser
|
9
|
-
def initialize(file_path)
|
9
|
+
def initialize(file_path, options = {})
|
10
10
|
@content = File.read(file_path)
|
11
11
|
@file_path = file_path.is_a?(Pathname) ? file_path.to_s : file_path
|
12
|
+
@options = options
|
12
13
|
end
|
13
14
|
|
14
15
|
def perform
|
15
16
|
template = ERB.new(@content)
|
16
17
|
template.filename = @file_path
|
17
|
-
|
18
|
+
permitted_classes = @options.fetch(:permitted_classes) { [] }
|
19
|
+
permitted_symbols = @options.fetch(:permitted_symbols) { [] }
|
20
|
+
aliases = @options.fetch(:aliases) { true }
|
21
|
+
# TODO: Psych in newer rubies expects these args to be keyword args.
|
22
|
+
YAML.safe_load template.result, permitted_classes, permitted_symbols, aliases
|
18
23
|
end
|
19
24
|
|
20
|
-
def self.perform(file_path)
|
21
|
-
new(file_path).perform
|
25
|
+
def self.perform(file_path, options = {})
|
26
|
+
new(file_path, options).perform
|
22
27
|
end
|
23
28
|
end
|
24
29
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module Hashie
|
2
2
|
module Extensions
|
3
|
-
# SRP: This extension will fail an error whenever a key is accessed
|
3
|
+
# SRP: This extension will fail an error whenever a key is accessed
|
4
|
+
# that does not exist in the hash.
|
4
5
|
#
|
5
6
|
# EXAMPLE:
|
6
7
|
#
|
@@ -15,12 +16,15 @@ module Hashie
|
|
15
16
|
# >> hash[:cow]
|
16
17
|
# KeyError: key not found: :cow
|
17
18
|
#
|
18
|
-
# NOTE: For googlers coming from Python to Ruby, this extension makes a Hash
|
19
|
+
# NOTE: For googlers coming from Python to Ruby, this extension makes a Hash
|
20
|
+
# behave more like a "Dictionary".
|
19
21
|
#
|
20
22
|
module StrictKeyAccess
|
21
23
|
class DefaultError < StandardError
|
22
|
-
def initialize
|
23
|
-
super
|
24
|
+
def initialize
|
25
|
+
super('Setting or using a default with Hashie::Extensions::StrictKeyAccess'\
|
26
|
+
' does not make sense'
|
27
|
+
)
|
24
28
|
end
|
25
29
|
end
|
26
30
|
|
data/lib/hashie/hash.rb
CHANGED
@@ -18,20 +18,21 @@ module Hashie
|
|
18
18
|
def to_hash(options = {})
|
19
19
|
out = {}
|
20
20
|
each_key do |k|
|
21
|
-
assignment_key =
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
21
|
+
assignment_key =
|
22
|
+
if options[:stringify_keys]
|
23
|
+
k.to_s
|
24
|
+
elsif options[:symbolize_keys]
|
25
|
+
k.to_s.to_sym
|
26
|
+
else
|
27
|
+
k
|
28
|
+
end
|
28
29
|
if self[k].is_a?(Array)
|
29
30
|
out[assignment_key] ||= []
|
30
31
|
self[k].each do |array_object|
|
31
|
-
out[assignment_key] << (array_object
|
32
|
+
out[assignment_key] << maybe_convert_to_hash(array_object, options)
|
32
33
|
end
|
33
34
|
else
|
34
|
-
out[assignment_key] =
|
35
|
+
out[assignment_key] = maybe_convert_to_hash(self[k], options)
|
35
36
|
end
|
36
37
|
end
|
37
38
|
out
|
@@ -44,6 +45,12 @@ module Hashie
|
|
44
45
|
|
45
46
|
private
|
46
47
|
|
48
|
+
def maybe_convert_to_hash(object, options)
|
49
|
+
return object unless object.is_a?(Hash) || object.respond_to?(:to_hash)
|
50
|
+
|
51
|
+
flexibly_convert_to_hash(object, options)
|
52
|
+
end
|
53
|
+
|
47
54
|
def flexibly_convert_to_hash(object, options = {})
|
48
55
|
if object.method(:to_hash).arity.zero?
|
49
56
|
object.to_hash
|