hashie 5.1.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -0
- data/CONTRIBUTING.md +38 -1
- data/README.md +816 -181
- data/Rakefile +29 -1
- data/UPGRADING.md +18 -0
- data/hashie.gemspec +8 -7
- data/lib/hashie/array.rb +15 -12
- data/lib/hashie/clash.rb +59 -4
- data/lib/hashie/dash.rb +144 -16
- data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
- data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
- data/lib/hashie/extensions/block_access.rb +71 -0
- data/lib/hashie/extensions/coercion.rb +180 -19
- data/lib/hashie/extensions/dash/coercion.rb +34 -3
- data/lib/hashie/extensions/dash/indifferent_access.rb +83 -5
- data/lib/hashie/extensions/dash/lazy.rb +170 -0
- data/lib/hashie/extensions/dash/predefined_values.rb +69 -9
- data/lib/hashie/extensions/dash/property_translation.rb +134 -16
- data/lib/hashie/extensions/deep_fetch.rb +21 -9
- data/lib/hashie/extensions/deep_find.rb +20 -0
- data/lib/hashie/extensions/deep_grep.rb +69 -0
- data/lib/hashie/extensions/deep_locate.rb +15 -28
- data/lib/hashie/extensions/deep_merge.rb +16 -0
- data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
- data/lib/hashie/extensions/ignore_undeclared.rb +21 -3
- data/lib/hashie/extensions/indifferent_access.rb +100 -14
- data/lib/hashie/extensions/kernel_coercion.rb +58 -0
- data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
- data/lib/hashie/extensions/key_conversion.rb +9 -0
- data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
- data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
- data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
- data/lib/hashie/extensions/mash/required_keys.rb +111 -0
- data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
- data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
- data/lib/hashie/extensions/mash/symbolize_keys.rb +6 -2
- data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
- data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
- data/lib/hashie/extensions/merge_initializer.rb +9 -0
- data/lib/hashie/extensions/method_access.rb +93 -7
- data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
- data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
- data/lib/hashie/extensions/persistable.rb +137 -0
- data/lib/hashie/extensions/pretty_inspect.rb +14 -0
- data/lib/hashie/extensions/ruby_version.rb +18 -1
- data/lib/hashie/extensions/ruby_version_check.rb +17 -0
- data/lib/hashie/extensions/strict_key_access.rb +59 -5
- data/lib/hashie/extensions/stringify_keys.rb +72 -17
- data/lib/hashie/extensions/symbolize_keys.rb +72 -16
- data/lib/hashie/hash.rb +16 -0
- data/lib/hashie/logger.rb +18 -1
- data/lib/hashie/mash.rb +293 -62
- data/lib/hashie/railtie.rb +27 -1
- data/lib/hashie/rash.rb +64 -4
- data/lib/hashie/trash.rb +8 -0
- data/lib/hashie/utils.rb +17 -6
- data/lib/hashie/version.rb +3 -1
- data/lib/hashie.rb +45 -0
- metadata +15 -17
|
@@ -1,7 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
module Mash
|
|
6
|
+
# Generates reusable methods as Mash properties are accessed.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# klass = Class.new(Hashie::Mash) do
|
|
10
|
+
# include Hashie::Extensions::Mash::DefineAccessors
|
|
11
|
+
# end
|
|
12
|
+
# klass.new(name: 'Ada').name # => 'Ada'
|
|
4
13
|
module DefineAccessors
|
|
14
|
+
# @example Install generated accessors on a subclass
|
|
15
|
+
# klass = Class.new(Hashie::Mash)
|
|
16
|
+
# klass.include(Hashie::Extensions::Mash::DefineAccessors)
|
|
17
|
+
# klass.new(quantity: 2).quantity # => 2
|
|
5
18
|
def self.included(klass)
|
|
6
19
|
klass.class_eval do
|
|
7
20
|
mod = Ext.new
|
|
@@ -9,23 +22,40 @@ module Hashie
|
|
|
9
22
|
end
|
|
10
23
|
end
|
|
11
24
|
|
|
25
|
+
# @example Install generated accessors on one Mash
|
|
26
|
+
# mash = Hashie::Mash.new(quantity: 2)
|
|
27
|
+
# mash.extend(Hashie::Extensions::Mash::DefineAccessors)
|
|
28
|
+
# mash.quantity # => 2
|
|
12
29
|
def self.extended(obj)
|
|
13
30
|
included(obj.singleton_class)
|
|
14
31
|
end
|
|
15
32
|
|
|
33
|
+
# A module of lazily generated accessors for a Mash class.
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# accessors = Hashie::Extensions::Mash::DefineAccessors::Ext.new
|
|
37
|
+
# klass = Class.new(Hashie::Mash) { include accessors }
|
|
38
|
+
# mash = klass.new(name: 'Ada')
|
|
39
|
+
# accessors.define_reader('name', :name)
|
|
40
|
+
# mash.name # => 'Ada'
|
|
16
41
|
class Ext < Module
|
|
42
|
+
# @example Generate a method on first access
|
|
43
|
+
# accessors = Hashie::Extensions::Mash::DefineAccessors::Ext.new
|
|
44
|
+
# klass = Class.new(Hashie::Mash) { include accessors }
|
|
45
|
+
# klass.new(name: 'Ada').name # => 'Ada'
|
|
46
|
+
# accessors.instance_methods(false).include?(:name) # => true
|
|
17
47
|
def initialize
|
|
18
48
|
mod = self
|
|
19
49
|
define_method(:method_missing) do |method_name, *args, &block|
|
|
20
50
|
key, suffix = method_name_and_suffix(method_name)
|
|
21
51
|
case suffix
|
|
22
|
-
when '='
|
|
52
|
+
when '='
|
|
23
53
|
mod.define_writer(key, method_name)
|
|
24
|
-
when '?'
|
|
54
|
+
when '?'
|
|
25
55
|
mod.define_predicate(key, method_name)
|
|
26
|
-
when '!'
|
|
56
|
+
when '!'
|
|
27
57
|
mod.define_initializing_reader(key, method_name)
|
|
28
|
-
when '_'
|
|
58
|
+
when '_'
|
|
29
59
|
mod.define_underbang_reader(key, method_name)
|
|
30
60
|
else
|
|
31
61
|
mod.define_reader(key, method_name)
|
|
@@ -34,6 +64,12 @@ module Hashie
|
|
|
34
64
|
end
|
|
35
65
|
end
|
|
36
66
|
|
|
67
|
+
# @example
|
|
68
|
+
# accessors = Hashie::Extensions::Mash::DefineAccessors::Ext.new
|
|
69
|
+
# klass = Class.new(Hashie::Mash) { include accessors }
|
|
70
|
+
# mash = klass.new(name: 'Ada')
|
|
71
|
+
# accessors.define_reader('name', :name)
|
|
72
|
+
# mash.name # => 'Ada'
|
|
37
73
|
def define_reader(key, method_name)
|
|
38
74
|
define_method(method_name) do |&block|
|
|
39
75
|
if key? method_name
|
|
@@ -44,40 +80,67 @@ module Hashie
|
|
|
44
80
|
end
|
|
45
81
|
end
|
|
46
82
|
|
|
83
|
+
# @example
|
|
84
|
+
# accessors = Hashie::Extensions::Mash::DefineAccessors::Ext.new
|
|
85
|
+
# accessors.define_writer('name', :name=)
|
|
86
|
+
# klass = Class.new(Hashie::Mash) { include accessors }
|
|
87
|
+
# mash = klass.new
|
|
88
|
+
# mash.name = 'Ada'
|
|
89
|
+
# mash['name'] # => 'Ada'
|
|
47
90
|
def define_writer(key, method_name)
|
|
48
91
|
define_method(method_name) do |value = nil|
|
|
49
92
|
if key? method_name
|
|
50
|
-
self.[](method_name
|
|
93
|
+
self.[](method_name)
|
|
51
94
|
else
|
|
52
95
|
assign_property(key, value)
|
|
53
96
|
end
|
|
54
97
|
end
|
|
55
98
|
end
|
|
56
99
|
|
|
100
|
+
# @example
|
|
101
|
+
# accessors = Hashie::Extensions::Mash::DefineAccessors::Ext.new
|
|
102
|
+
# accessors.define_predicate('name', :name?)
|
|
103
|
+
# klass = Class.new(Hashie::Mash) { include accessors }
|
|
104
|
+
# klass.new(name: 'Ada').name? # => true
|
|
105
|
+
# klass.new.name? # => false
|
|
57
106
|
def define_predicate(key, method_name)
|
|
58
107
|
define_method(method_name) do
|
|
59
108
|
if key? method_name
|
|
60
|
-
self.[](method_name
|
|
109
|
+
self.[](method_name)
|
|
61
110
|
else
|
|
62
111
|
!!self[key]
|
|
63
112
|
end
|
|
64
113
|
end
|
|
65
114
|
end
|
|
66
115
|
|
|
116
|
+
# @example
|
|
117
|
+
# accessors = Hashie::Extensions::Mash::DefineAccessors::Ext.new
|
|
118
|
+
# accessors.define_initializing_reader('address', :address!)
|
|
119
|
+
# klass = Class.new(Hashie::Mash) { include accessors }
|
|
120
|
+
# mash = klass.new
|
|
121
|
+
# mash.address!.city = 'London'
|
|
122
|
+
# mash.address.city # => 'London'
|
|
67
123
|
def define_initializing_reader(key, method_name)
|
|
68
124
|
define_method(method_name) do
|
|
69
125
|
if key? method_name
|
|
70
|
-
self.[](method_name
|
|
126
|
+
self.[](method_name)
|
|
71
127
|
else
|
|
72
128
|
initializing_reader(key)
|
|
73
129
|
end
|
|
74
130
|
end
|
|
75
131
|
end
|
|
76
132
|
|
|
133
|
+
# @example
|
|
134
|
+
# accessors = Hashie::Extensions::Mash::DefineAccessors::Ext.new
|
|
135
|
+
# accessors.define_underbang_reader('address', :address_)
|
|
136
|
+
# klass = Class.new(Hashie::Mash) { include accessors }
|
|
137
|
+
# mash = klass.new
|
|
138
|
+
# mash.address_.city = 'London'
|
|
139
|
+
# mash.key?('address') # => false
|
|
77
140
|
def define_underbang_reader(key, method_name)
|
|
78
141
|
define_method(method_name) do
|
|
79
142
|
if key? method_name
|
|
80
|
-
self.[](key
|
|
143
|
+
self.[](key)
|
|
81
144
|
else
|
|
82
145
|
underbang_reader(key)
|
|
83
146
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
module Mash
|
|
@@ -14,8 +16,13 @@ module Hashie
|
|
|
14
16
|
# mash['string_key'] == mash[:string_key] #=> true
|
|
15
17
|
# mash[:symbol_key] == mash['symbol_key'] #=> true
|
|
16
18
|
module KeepOriginalKeys
|
|
19
|
+
# @example Preserve symbol keys when including the extension
|
|
20
|
+
# klass = Class.new(Hashie::Mash) do
|
|
21
|
+
# include Hashie::Extensions::Mash::KeepOriginalKeys
|
|
22
|
+
# end
|
|
23
|
+
# klass.new(name: 'Ada').keys # => [:name]
|
|
17
24
|
def self.included(descendant)
|
|
18
|
-
error_message = "
|
|
25
|
+
error_message = "`#{descendant}` is not a kind of Hashie::Mash"
|
|
19
26
|
raise ArgumentError, error_message unless descendant <= Hashie::Mash
|
|
20
27
|
end
|
|
21
28
|
|
|
@@ -26,13 +33,10 @@ module Hashie
|
|
|
26
33
|
# @param [Object, String, Symbol] key the key to access.
|
|
27
34
|
# @return [Object] the value assigned to the key.
|
|
28
35
|
def convert_key(key)
|
|
29
|
-
if regular_key?(key)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
else
|
|
34
|
-
key
|
|
35
|
-
end
|
|
36
|
+
return key if regular_key?(key)
|
|
37
|
+
|
|
38
|
+
converted_key = __convert(key)
|
|
39
|
+
converted_key && regular_key?(converted_key) ? converted_key : key
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
# Converts symbol/string keys to their alternative formats, but leaves
|
|
@@ -42,7 +46,7 @@ module Hashie
|
|
|
42
46
|
# @return [Object, String, Symbol] the converted key.
|
|
43
47
|
def __convert(key)
|
|
44
48
|
case key
|
|
45
|
-
when Symbol then key.
|
|
49
|
+
when Symbol then key.name
|
|
46
50
|
when String then key.to_sym
|
|
47
51
|
else key
|
|
48
52
|
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hashie
|
|
4
|
+
module Extensions
|
|
5
|
+
module Mash
|
|
6
|
+
# Extends a Mash with the ability to require specific keys to be
|
|
7
|
+
# set (non-nil) when constructed, while still allowing arbitrary,
|
|
8
|
+
# undeclared keys to be assigned - unlike Hashie::Dash, which
|
|
9
|
+
# restricts assignment to only pre-declared properties.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
#
|
|
13
|
+
# class PersonMash < Hashie::Mash
|
|
14
|
+
# include Hashie::Extensions::Mash::RequiredKeys
|
|
15
|
+
#
|
|
16
|
+
# required_keys :name, :email
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# expect { PersonMash.new(email: 'bob@example.com') }
|
|
20
|
+
# .to raise_error(ArgumentError, 'the following keys are required: `name`')
|
|
21
|
+
#
|
|
22
|
+
# person = PersonMash.new(name: 'Bob', email: 'bob@example.com')
|
|
23
|
+
# person.age = 42 # arbitrary, undeclared keys are still allowed
|
|
24
|
+
# person.age # => 42
|
|
25
|
+
module RequiredKeys
|
|
26
|
+
# @example Install required-key declarations
|
|
27
|
+
# klass = Class.new(Hashie::Mash) do
|
|
28
|
+
# include Hashie::Extensions::Mash::RequiredKeys
|
|
29
|
+
# end
|
|
30
|
+
# klass.required_keys # => []
|
|
31
|
+
def self.included(base)
|
|
32
|
+
base.instance_variable_set(:@required_keys, [])
|
|
33
|
+
base.extend(ClassMethods)
|
|
34
|
+
base.include(InstanceMethods)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Class-level required-key declarations inherited by subclasses.
|
|
38
|
+
#
|
|
39
|
+
# @example
|
|
40
|
+
# klass = Class.new(Hashie::Mash) do
|
|
41
|
+
# include Hashie::Extensions::Mash::RequiredKeys
|
|
42
|
+
# required_keys :name
|
|
43
|
+
# end
|
|
44
|
+
# klass.required_keys # => ['name']
|
|
45
|
+
module ClassMethods
|
|
46
|
+
# @example Copy required keys when subclassing
|
|
47
|
+
# parent = Class.new(Hashie::Mash) do
|
|
48
|
+
# include Hashie::Extensions::Mash::RequiredKeys
|
|
49
|
+
# required_keys :name
|
|
50
|
+
# end
|
|
51
|
+
# child = Class.new(parent)
|
|
52
|
+
# child.required_keys :email
|
|
53
|
+
# child.required_keys # => ['name', 'email']
|
|
54
|
+
# parent.required_keys # => ['name']
|
|
55
|
+
def inherited(klass)
|
|
56
|
+
super
|
|
57
|
+
klass.instance_variable_set(:@required_keys, required_keys.dup)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# With no arguments, returns the keys currently declared as
|
|
61
|
+
# required. With one or more arguments, declares those keys
|
|
62
|
+
# as required. Keys are stored and compared as strings, so
|
|
63
|
+
# both symbol and string keys are accepted.
|
|
64
|
+
#
|
|
65
|
+
# @example
|
|
66
|
+
# klass = Class.new(Hashie::Mash) do
|
|
67
|
+
# include Hashie::Extensions::Mash::RequiredKeys
|
|
68
|
+
# required_keys :name, 'email', :name
|
|
69
|
+
# end
|
|
70
|
+
# klass.required_keys # => ['name', 'email']
|
|
71
|
+
def required_keys(*keys)
|
|
72
|
+
return @required_keys if keys.empty?
|
|
73
|
+
|
|
74
|
+
@required_keys |= keys.map(&:to_s)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Checks required values when constructing a Mash.
|
|
79
|
+
#
|
|
80
|
+
# @example
|
|
81
|
+
# klass = Class.new(Hashie::Mash) do
|
|
82
|
+
# include Hashie::Extensions::Mash::RequiredKeys
|
|
83
|
+
# required_keys :name
|
|
84
|
+
# end
|
|
85
|
+
# klass.new(name: 'Ada').name # => 'Ada'
|
|
86
|
+
module InstanceMethods
|
|
87
|
+
# @example Reject nil or missing required values
|
|
88
|
+
# klass = Class.new(Hashie::Mash) do
|
|
89
|
+
# include Hashie::Extensions::Mash::RequiredKeys
|
|
90
|
+
# required_keys :name
|
|
91
|
+
# end
|
|
92
|
+
# expect { klass.new(name: nil) }.to raise_error(ArgumentError, /name/)
|
|
93
|
+
# klass.new(name: 'Ada', age: 37).age # => 37
|
|
94
|
+
def initialize(*args, &block)
|
|
95
|
+
super
|
|
96
|
+
assert_required_keys_set!
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def assert_required_keys_set!
|
|
102
|
+
missing_keys = self.class.required_keys.select { |key| self[key].nil? }
|
|
103
|
+
return if missing_keys.empty?
|
|
104
|
+
|
|
105
|
+
raise ArgumentError, "the following keys are required: `#{missing_keys.join(', ')}`"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -1,14 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
module Mash
|
|
6
|
+
# Rejects assignments that would shadow existing Mash methods.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# klass = Class.new(Hashie::Mash) do
|
|
10
|
+
# include Hashie::Extensions::Mash::SafeAssignment
|
|
11
|
+
# end
|
|
12
|
+
# mash = klass.new
|
|
13
|
+
# expect { mash[:size] = 2 }.to raise_error(ArgumentError, /clashes/)
|
|
14
|
+
# mash.empty? # => true
|
|
4
15
|
module SafeAssignment
|
|
5
|
-
def custom_writer(key, *args)
|
|
6
|
-
if !key?(key) && respond_to?(key, true)
|
|
7
|
-
|
|
8
|
-
end
|
|
16
|
+
def custom_writer(key, *args) # :nodoc:
|
|
17
|
+
raise ArgumentError, "the property `#{key}` clashes with an existing method" if !key?(key) && respond_to?(key, true)
|
|
18
|
+
|
|
9
19
|
super
|
|
10
20
|
end
|
|
11
21
|
|
|
22
|
+
# @example
|
|
23
|
+
# klass = Class.new(Hashie::Mash) do
|
|
24
|
+
# include Hashie::Extensions::Mash::SafeAssignment
|
|
25
|
+
# end
|
|
26
|
+
# mash = klass.new
|
|
27
|
+
# mash[:name] = 'Ada'
|
|
28
|
+
# mash.name # => 'Ada'
|
|
29
|
+
# expect { mash[:size] = 2 }.to raise_error(ArgumentError, /clashes/)
|
|
12
30
|
def []=(*args)
|
|
13
31
|
custom_writer(*args)
|
|
14
32
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hashie
|
|
4
|
+
module Extensions
|
|
5
|
+
module Mash
|
|
6
|
+
# Overrides Mash's default behavior of returning nil for keys that
|
|
7
|
+
# aren't set, raising a KeyError instead.
|
|
8
|
+
#
|
|
9
|
+
# Unlike Hashie::Extensions::StrictKeyAccess, this extension is aware
|
|
10
|
+
# of Mash's special suffixed methods (e.g. +?+, +!+, +_+ and +=+) so
|
|
11
|
+
# that they continue to work as expected instead of always raising.
|
|
12
|
+
#
|
|
13
|
+
# Because nested Hashes are converted into instances of +self.class+,
|
|
14
|
+
# this extension is automatically applied to any nested Mash as well.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# class StrictMash < Hashie::Mash
|
|
18
|
+
# include Hashie::Extensions::Mash::StrictKeyAccess
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# mash = StrictMash.new(name: 'Bob')
|
|
22
|
+
# mash.name #=> 'Bob'
|
|
23
|
+
# expect { mash.age }.to raise_error(KeyError, 'key not found: `age`')
|
|
24
|
+
#
|
|
25
|
+
# mash.name? #=> true
|
|
26
|
+
# mash.age? #=> false
|
|
27
|
+
#
|
|
28
|
+
# mash.address!.city = 'Springfield'
|
|
29
|
+
# mash.address.city #=> 'Springfield'
|
|
30
|
+
#
|
|
31
|
+
# @api public
|
|
32
|
+
module StrictKeyAccess
|
|
33
|
+
# Hook for being included in a class
|
|
34
|
+
#
|
|
35
|
+
# @api private
|
|
36
|
+
# @return [void]
|
|
37
|
+
# @raise [ArgumentError] when the base class isn't a Mash
|
|
38
|
+
def self.included(descendant)
|
|
39
|
+
error_message = "#{descendant} is not a kind of Hashie::Mash"
|
|
40
|
+
raise ArgumentError, error_message unless descendant <= Hashie::Mash
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @api private
|
|
44
|
+
def method_missing(method_name, *args, &blk)
|
|
45
|
+
return super if key?(method_name)
|
|
46
|
+
|
|
47
|
+
name, suffix = method_name_and_suffix(method_name)
|
|
48
|
+
raise KeyError, "key not found: `#{name}`" if !key?(name) && !suffix
|
|
49
|
+
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @api private
|
|
54
|
+
def respond_to_missing?(method_name, *args)
|
|
55
|
+
return true if key?(method_name)
|
|
56
|
+
|
|
57
|
+
_, suffix = method_name_and_suffix(method_name)
|
|
58
|
+
return false unless suffix
|
|
59
|
+
|
|
60
|
+
super
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
module Mash
|
|
@@ -9,7 +11,7 @@ module Hashie
|
|
|
9
11
|
# end
|
|
10
12
|
#
|
|
11
13
|
# response = LazyResponse.new("id" => 123, "name" => "Rey").to_h
|
|
12
|
-
#
|
|
14
|
+
# response # => {id: 123, name: "Rey"}
|
|
13
15
|
#
|
|
14
16
|
# @api public
|
|
15
17
|
module SymbolizeKeys
|
|
@@ -19,7 +21,9 @@ module Hashie
|
|
|
19
21
|
# @return [void]
|
|
20
22
|
# @raise [ArgumentError] when the base class isn't a Mash
|
|
21
23
|
def self.included(base)
|
|
22
|
-
|
|
24
|
+
return if base <= Hashie::Mash
|
|
25
|
+
|
|
26
|
+
raise ArgumentError, "`#{base}` must descend from Hashie::Mash"
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
private
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hashie
|
|
4
|
+
module Extensions
|
|
5
|
+
module Mash
|
|
6
|
+
# Applies a class-configured normalization to string and symbol Mash keys.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# klass = Class.new(Hashie::Mash) do
|
|
10
|
+
# include Hashie::Extensions::Mash::TransformKeys
|
|
11
|
+
# transform_keys_with(&:downcase)
|
|
12
|
+
# end
|
|
13
|
+
# klass.new('NAME' => 'Ada').name # => 'Ada'
|
|
14
|
+
module TransformKeys
|
|
15
|
+
# @example Install class-level transformation configuration
|
|
16
|
+
# klass = Class.new(Hashie::Mash) do
|
|
17
|
+
# include Hashie::Extensions::Mash::TransformKeys
|
|
18
|
+
# end
|
|
19
|
+
# klass.key_transformer # => nil
|
|
20
|
+
def self.included(base)
|
|
21
|
+
raise ArgumentError, "`#{base}` must descend from Hashie::Mash" unless base <= Hashie::Mash
|
|
22
|
+
|
|
23
|
+
base.extend(ClassMethods)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Configures key normalization independently for each subclass.
|
|
27
|
+
#
|
|
28
|
+
# @example
|
|
29
|
+
# klass = Class.new(Hashie::Mash) do
|
|
30
|
+
# include Hashie::Extensions::Mash::TransformKeys
|
|
31
|
+
# transform_keys_with(&:downcase)
|
|
32
|
+
# end
|
|
33
|
+
# klass.new('NAME' => 'Ada')['name'] # => 'Ada'
|
|
34
|
+
module ClassMethods
|
|
35
|
+
# @example Inherit a transformer without sharing later configuration
|
|
36
|
+
# parent = Class.new(Hashie::Mash) do
|
|
37
|
+
# include Hashie::Extensions::Mash::TransformKeys
|
|
38
|
+
# transform_keys_with(&:downcase)
|
|
39
|
+
# end
|
|
40
|
+
# child = Class.new(parent)
|
|
41
|
+
# child.new('NAME' => 'Ada').name # => 'Ada'
|
|
42
|
+
# child.transform_keys_with(&:upcase)
|
|
43
|
+
# parent.new('NAME' => 'Ada').keys # => ['name']
|
|
44
|
+
def inherited(klass)
|
|
45
|
+
super
|
|
46
|
+
klass.instance_variable_set(:@key_transformer, key_transformer)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @example Configure with a callable or a block
|
|
50
|
+
# klass = Class.new(Hashie::Mash) do
|
|
51
|
+
# include Hashie::Extensions::Mash::TransformKeys
|
|
52
|
+
# transform_keys_with(->(key) { key.downcase })
|
|
53
|
+
# end
|
|
54
|
+
# klass.new('NAME' => 'Ada').keys # => ['name']
|
|
55
|
+
# klass.transform_keys_with { |key| key.upcase }
|
|
56
|
+
# klass.new(name: 'Ada').keys # => ['NAME']
|
|
57
|
+
def transform_keys_with(callable = nil, &block)
|
|
58
|
+
raise ArgumentError, 'provide one callable or block' if (callable && block) || !(callable || block).respond_to?(:call)
|
|
59
|
+
|
|
60
|
+
@key_transformer = callable || block
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @example Read the configured callable
|
|
64
|
+
# klass = Class.new(Hashie::Mash) do
|
|
65
|
+
# include Hashie::Extensions::Mash::TransformKeys
|
|
66
|
+
# transform_keys_with(&:downcase)
|
|
67
|
+
# end
|
|
68
|
+
# klass.key_transformer.call('NAME') # => 'name'
|
|
69
|
+
attr_reader :key_transformer
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def convert_key(key)
|
|
75
|
+
transformer = self.class.key_transformer
|
|
76
|
+
return super unless transformer && (key.is_a?(String) || key.is_a?(Symbol))
|
|
77
|
+
|
|
78
|
+
transformed = transformer.call(key.to_s)
|
|
79
|
+
raise TypeError, 'key transformer must return a String' unless transformed.is_a?(String)
|
|
80
|
+
|
|
81
|
+
super(transformed)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'hashie/extensions/mash/transform_keys'
|
|
4
|
+
|
|
5
|
+
module Hashie
|
|
6
|
+
module Extensions
|
|
7
|
+
module Mash
|
|
8
|
+
# Normalizes camel-case and hyphenated Mash keys to lowercase underscores.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# klass = Class.new(Hashie::Mash) do
|
|
12
|
+
# include Hashie::Extensions::Mash::UnderscoreKeys
|
|
13
|
+
# end
|
|
14
|
+
# klass.new('firstName' => 'Ada', 'last-name' => 'Lovelace').to_h # => {'first_name' => 'Ada', 'last_name' => 'Lovelace'}
|
|
15
|
+
module UnderscoreKeys
|
|
16
|
+
# @example Configure underscore normalization on inclusion
|
|
17
|
+
# klass = Class.new(Hashie::Mash) do
|
|
18
|
+
# include Hashie::Extensions::Mash::UnderscoreKeys
|
|
19
|
+
# end
|
|
20
|
+
# klass.key_transformer.call('HTTPStatus') # => 'http_status'
|
|
21
|
+
def self.included(base)
|
|
22
|
+
base.include(TransformKeys) unless base < TransformKeys
|
|
23
|
+
base.transform_keys_with do |key|
|
|
24
|
+
key.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
25
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
26
|
+
.tr('-', '_').downcase
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
# The MergeInitializer is a super-simple mixin that allows
|
|
@@ -15,6 +17,13 @@ module Hashie
|
|
|
15
17
|
# h[:abc] # => 'def'
|
|
16
18
|
#
|
|
17
19
|
module MergeInitializer
|
|
20
|
+
# @example Initialize entries and a default value
|
|
21
|
+
# class InitializedHash < Hash
|
|
22
|
+
# include Hashie::Extensions::MergeInitializer
|
|
23
|
+
# end
|
|
24
|
+
# hash = InitializedHash.new({ name: 'Ada' }, 'unknown')
|
|
25
|
+
# hash[:name] # => 'Ada'
|
|
26
|
+
# hash[:missing] # => 'unknown'
|
|
18
27
|
def initialize(hash = {}, default = nil, &block)
|
|
19
28
|
default ? super(default) : super(&block)
|
|
20
29
|
hash.each do |key, value|
|