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
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hashie
|
|
4
|
+
module Extensions
|
|
5
|
+
module Dash
|
|
6
|
+
# Loads declared properties on demand from an initializer block.
|
|
7
|
+
# Include after other Dash extensions. Hash enumeration only sees loaded
|
|
8
|
+
# entries; to_h loads every declared property.
|
|
9
|
+
#
|
|
10
|
+
# @example Load and memoize a property
|
|
11
|
+
# klass = Class.new(Hashie::Dash) do
|
|
12
|
+
# include Hashie::Extensions::Dash::Lazy
|
|
13
|
+
# property :name
|
|
14
|
+
# end
|
|
15
|
+
# calls = []
|
|
16
|
+
# dash = klass.new { |key| calls << key; 'Ada' }
|
|
17
|
+
# calls # => []
|
|
18
|
+
# dash.name # => 'Ada'
|
|
19
|
+
# dash.name # => 'Ada'
|
|
20
|
+
# calls # => [:name]
|
|
21
|
+
module Lazy
|
|
22
|
+
# @example Hash initialization without a loader retains normal behavior
|
|
23
|
+
# klass = Class.new(Hashie::Dash) do
|
|
24
|
+
# include Hashie::Extensions::Dash::Lazy
|
|
25
|
+
# property :name, required: true
|
|
26
|
+
# end
|
|
27
|
+
# klass.new(name: 'Ada').name # => 'Ada'
|
|
28
|
+
def initialize(attributes = {}, &loader)
|
|
29
|
+
@lazy_loader = loader
|
|
30
|
+
@lazy_loading = []
|
|
31
|
+
super(attributes, &nil)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @example Read a property without loading its neighbors
|
|
35
|
+
# klass = Class.new(Hashie::Dash) do
|
|
36
|
+
# include Hashie::Extensions::Dash::Lazy
|
|
37
|
+
# property :name
|
|
38
|
+
# property :age
|
|
39
|
+
# end
|
|
40
|
+
# dash = klass.new { |key| { name: 'Ada', age: 37 }[key] }
|
|
41
|
+
# dash[:name] # => 'Ada'
|
|
42
|
+
# dash.keys # => [:name]
|
|
43
|
+
def [](property, &block)
|
|
44
|
+
return super unless @lazy_loader
|
|
45
|
+
|
|
46
|
+
assert_property_exists!(property)
|
|
47
|
+
canonical_property = lazy_property(property)
|
|
48
|
+
load_property(canonical_property) unless key?(property)
|
|
49
|
+
value = super
|
|
50
|
+
assert_property_required!(canonical_property, value)
|
|
51
|
+
value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @example Fetch a declared value through its loader
|
|
55
|
+
# klass = Class.new(Hashie::Dash) do
|
|
56
|
+
# include Hashie::Extensions::Dash::Lazy
|
|
57
|
+
# property :name
|
|
58
|
+
# end
|
|
59
|
+
# klass.new { |_key| 'Ada' }.fetch(:name) # => 'Ada'
|
|
60
|
+
def fetch(property, *args, &block)
|
|
61
|
+
self[property] if @lazy_loader && self.class.property?(property)
|
|
62
|
+
super
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @example Load the first property before digging into its value
|
|
66
|
+
# klass = Class.new(Hashie::Dash) do
|
|
67
|
+
# include Hashie::Extensions::Dash::Lazy
|
|
68
|
+
# property :user
|
|
69
|
+
# end
|
|
70
|
+
# klass.new { |_key| { name: 'Ada' } }.dig(:user, :name) # => 'Ada'
|
|
71
|
+
def dig(property, *path)
|
|
72
|
+
self[property] if @lazy_loader && self.class.property?(property)
|
|
73
|
+
super
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @example Convert all declared properties to a plain hash
|
|
77
|
+
# klass = Class.new(Hashie::Dash) do
|
|
78
|
+
# include Hashie::Extensions::Dash::Lazy
|
|
79
|
+
# property :name
|
|
80
|
+
# end
|
|
81
|
+
# klass.new { |_key| 'Ada' }.to_h # => { name: 'Ada' }
|
|
82
|
+
def to_h
|
|
83
|
+
self.class.properties.each { |property| self[property] } if @lazy_loader
|
|
84
|
+
super
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @example Convert all declared properties for hash coercion
|
|
88
|
+
# klass = Class.new(Hashie::Dash) do
|
|
89
|
+
# include Hashie::Extensions::Dash::Lazy
|
|
90
|
+
# property :name
|
|
91
|
+
# end
|
|
92
|
+
# klass.new { |_key| 'Ada' }.to_hash # => { name: 'Ada' }
|
|
93
|
+
alias to_hash to_h
|
|
94
|
+
|
|
95
|
+
# @example Freezing resolves pending values before preventing mutation
|
|
96
|
+
# klass = Class.new(Hashie::Dash) do
|
|
97
|
+
# include Hashie::Extensions::Dash::Lazy
|
|
98
|
+
# property :name
|
|
99
|
+
# end
|
|
100
|
+
# klass.new { |_key| 'Ada' }.freeze.name # => 'Ada'
|
|
101
|
+
def freeze
|
|
102
|
+
to_h if @lazy_loader && !frozen?
|
|
103
|
+
super
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def initialize_attributes(attributes)
|
|
109
|
+
return super unless @lazy_loader
|
|
110
|
+
|
|
111
|
+
@lazy_attributes = attributes || {}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def assert_required_attributes_set!
|
|
115
|
+
super unless @lazy_loader
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def initialize_copy(other)
|
|
119
|
+
super
|
|
120
|
+
@lazy_loading = []
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def lazy_property(property)
|
|
124
|
+
return property if self.class.properties.include?(property)
|
|
125
|
+
|
|
126
|
+
self.class.properties.find { |key| key.to_s == property.to_s } || property
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def lazy_attribute_key(property)
|
|
130
|
+
return property if @lazy_attributes.key?(property)
|
|
131
|
+
return unless respond_to?(:convert_key)
|
|
132
|
+
|
|
133
|
+
converted = convert_key(property)
|
|
134
|
+
converted if @lazy_attributes.key?(converted)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def load_property(property)
|
|
138
|
+
raise ArgumentError, "circular lazy loading for property `#{property}`" if @lazy_loading.include?(property)
|
|
139
|
+
|
|
140
|
+
@lazy_loading << property
|
|
141
|
+
begin
|
|
142
|
+
source = if self.class.respond_to?(:inverse_translations)
|
|
143
|
+
self.class.inverse_translations.fetch(property, property)
|
|
144
|
+
else
|
|
145
|
+
property
|
|
146
|
+
end
|
|
147
|
+
attribute = lazy_attribute_key(property)
|
|
148
|
+
if attribute
|
|
149
|
+
source = property
|
|
150
|
+
value = @lazy_attributes[attribute]
|
|
151
|
+
else
|
|
152
|
+
attribute = lazy_attribute_key(source)
|
|
153
|
+
value = attribute ? @lazy_attributes[attribute] : @lazy_loader.call(source)
|
|
154
|
+
end
|
|
155
|
+
if value.nil?
|
|
156
|
+
value = self.class.defaults[property].dup
|
|
157
|
+
value = value.arity == 1 ? value.call(self) : value.call if value.is_a?(Proc)
|
|
158
|
+
elsif source != property
|
|
159
|
+
translator = self.class.translations_hash[source][property]
|
|
160
|
+
value = translator.call(value) if translator.respond_to?(:call)
|
|
161
|
+
end
|
|
162
|
+
self[property] = value
|
|
163
|
+
ensure
|
|
164
|
+
@lazy_loading.pop
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
module Dash
|
|
4
6
|
# Extends a Dash with the ability to accept only predefined values on a property.
|
|
5
7
|
#
|
|
6
|
-
#
|
|
8
|
+
# @example
|
|
7
9
|
#
|
|
8
10
|
# class PersonHash < Hashie::Dash
|
|
9
11
|
# include Hashie::Extensions::Dash::PredefinedValues
|
|
@@ -12,23 +14,59 @@ module Hashie
|
|
|
12
14
|
# property :age, values: (0..150) # a Range
|
|
13
15
|
# end
|
|
14
16
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
+
# expect { PersonHash.new(gender: :male, age: -1) }
|
|
18
|
+
# .to raise_error(ArgumentError, /value `-1` is not allowed for `age`/)
|
|
19
|
+
# PersonHash.new(gender: :male, age: 42).age # => 42
|
|
17
20
|
module PredefinedValues
|
|
21
|
+
# @example Install predefined-value declarations
|
|
22
|
+
# klass = Class.new(Hashie::Dash) do
|
|
23
|
+
# include Hashie::Extensions::Dash::PredefinedValues
|
|
24
|
+
# end
|
|
25
|
+
# klass.values_for_properties # => {}
|
|
18
26
|
def self.included(base)
|
|
19
27
|
base.instance_variable_set(:@values_for_properties, {})
|
|
20
28
|
base.extend(ClassMethods)
|
|
21
29
|
base.include(InstanceMethods)
|
|
22
30
|
end
|
|
23
31
|
|
|
32
|
+
# Declares and inherits permitted values for properties.
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# klass = Class.new(Hashie::Dash) do
|
|
36
|
+
# include Hashie::Extensions::Dash::PredefinedValues
|
|
37
|
+
# property :status, values: [:open, :closed]
|
|
38
|
+
# end
|
|
39
|
+
# klass.values_for_properties # => {status: [:open, :closed]}
|
|
24
40
|
module ClassMethods
|
|
41
|
+
# @example Read the constraints for declared properties
|
|
42
|
+
# klass = Class.new(Hashie::Dash) do
|
|
43
|
+
# include Hashie::Extensions::Dash::PredefinedValues
|
|
44
|
+
# property :age, values: (0..150)
|
|
45
|
+
# end
|
|
46
|
+
# klass.values_for_properties # => {age: (0..150)}
|
|
25
47
|
attr_reader :values_for_properties
|
|
26
48
|
|
|
49
|
+
# @example Inherit constraints without sharing the constraint map
|
|
50
|
+
# parent = Class.new(Hashie::Dash) do
|
|
51
|
+
# include Hashie::Extensions::Dash::PredefinedValues
|
|
52
|
+
# property :status, values: [:open, :closed]
|
|
53
|
+
# end
|
|
54
|
+
# child = Class.new(parent) { property :age, values: (0..150) }
|
|
55
|
+
# child.values_for_properties.keys # => [:status, :age]
|
|
56
|
+
# parent.values_for_properties.keys # => [:status]
|
|
27
57
|
def inherited(klass)
|
|
28
58
|
super
|
|
29
59
|
klass.instance_variable_set(:@values_for_properties, values_for_properties.dup)
|
|
30
60
|
end
|
|
31
61
|
|
|
62
|
+
# @example Accept an array or range of values
|
|
63
|
+
# klass = Class.new(Hashie::Dash) do
|
|
64
|
+
# include Hashie::Extensions::Dash::PredefinedValues
|
|
65
|
+
# property :status, values: [:open, :closed]
|
|
66
|
+
# property :age, values: (0..150)
|
|
67
|
+
# end
|
|
68
|
+
# klass.new(status: :open, age: 42).age # => 42
|
|
69
|
+
# expect { klass.new(status: :unknown) }.to raise_error(ArgumentError, /status/)
|
|
32
70
|
def property(property_name, options = {})
|
|
33
71
|
super
|
|
34
72
|
|
|
@@ -43,7 +81,7 @@ module Hashie
|
|
|
43
81
|
def assert_predefined_values!(predefined_values)
|
|
44
82
|
return if supported_type?(predefined_values)
|
|
45
83
|
|
|
46
|
-
raise ArgumentError, %(`values` accepts an Array or a Range
|
|
84
|
+
raise ArgumentError, %(`values` accepts an `Array` or a `Range`)
|
|
47
85
|
end
|
|
48
86
|
|
|
49
87
|
def supported_type?(predefined_values)
|
|
@@ -55,7 +93,22 @@ module Hashie
|
|
|
55
93
|
end
|
|
56
94
|
end
|
|
57
95
|
|
|
96
|
+
# Validates property constraints when constructing a Dash.
|
|
97
|
+
#
|
|
98
|
+
# @example
|
|
99
|
+
# klass = Class.new(Hashie::Dash) do
|
|
100
|
+
# include Hashie::Extensions::Dash::PredefinedValues
|
|
101
|
+
# property :status, values: [:open, :closed]
|
|
102
|
+
# end
|
|
103
|
+
# klass.new(status: :open).status # => :open
|
|
58
104
|
module InstanceMethods
|
|
105
|
+
# @example Reject values outside the declared set
|
|
106
|
+
# klass = Class.new(Hashie::Dash) do
|
|
107
|
+
# include Hashie::Extensions::Dash::PredefinedValues
|
|
108
|
+
# property :status, values: [:open, :closed]
|
|
109
|
+
# end
|
|
110
|
+
# expect { klass.new(status: :unknown) }.to raise_error(ArgumentError, /status/)
|
|
111
|
+
# klass.new.status # => nil
|
|
59
112
|
def initialize(*)
|
|
60
113
|
super
|
|
61
114
|
|
|
@@ -68,14 +121,21 @@ module Hashie
|
|
|
68
121
|
self.class.values_for_properties.each_key do |property|
|
|
69
122
|
value = send(property)
|
|
70
123
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
end
|
|
124
|
+
allowed_values = values_for_properties(property)
|
|
125
|
+
fail_property_value_error!(property, value, allowed_values) if value && !allowed_values.include?(value)
|
|
74
126
|
end
|
|
75
127
|
end
|
|
76
128
|
|
|
77
|
-
def fail_property_value_error!(property)
|
|
78
|
-
|
|
129
|
+
def fail_property_value_error!(property, value, allowed_values)
|
|
130
|
+
expected_values = if allowed_values.is_a?(::Range)
|
|
131
|
+
"a value in `#{allowed_values}`"
|
|
132
|
+
else
|
|
133
|
+
"one of `#{allowed_values.inspect}`"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
raise ArgumentError,
|
|
137
|
+
"value `#{value.inspect}` is not allowed for `#{property}`; " \
|
|
138
|
+
"expected #{expected_values}"
|
|
79
139
|
end
|
|
80
140
|
|
|
81
141
|
def values_for_properties(property)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
module Dash
|
|
@@ -7,12 +9,12 @@ module Hashie
|
|
|
7
9
|
# application -- such as a Java API -- where the keys are named
|
|
8
10
|
# differently from Ruby conventions.
|
|
9
11
|
#
|
|
10
|
-
#
|
|
12
|
+
# @example Translate inconsistent API keys
|
|
11
13
|
#
|
|
12
14
|
# class PersonHash < Hashie::Dash
|
|
13
15
|
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
14
16
|
#
|
|
15
|
-
# property :first_name, from :firstName
|
|
17
|
+
# property :first_name, from: :firstName
|
|
16
18
|
# property :last_name, from: :lastName
|
|
17
19
|
# property :first_name, from: :f_name
|
|
18
20
|
# property :last_name, from: :l_name
|
|
@@ -25,7 +27,8 @@ module Hashie
|
|
|
25
27
|
# You can also use a lambda to translate the value. This is particularly
|
|
26
28
|
# useful when you want to ensure the type of data you're wrapping.
|
|
27
29
|
#
|
|
28
|
-
#
|
|
30
|
+
# @example Transform input values
|
|
31
|
+
# require 'time'
|
|
29
32
|
#
|
|
30
33
|
# class DataModelHash < Hashie::Dash
|
|
31
34
|
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
@@ -35,28 +38,66 @@ module Hashie
|
|
|
35
38
|
# end
|
|
36
39
|
#
|
|
37
40
|
# model = DataModelHash.new(id: '123', created: '2014-04-25 22:35:28')
|
|
38
|
-
# model.id.class #=> Integer
|
|
41
|
+
# model.id.class #=> Integer
|
|
39
42
|
# model.created_at.class #=> Time
|
|
40
43
|
module PropertyTranslation
|
|
44
|
+
# @example Install translation declarations on a Dash
|
|
45
|
+
# klass = Class.new(Hashie::Dash) do
|
|
46
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
47
|
+
# property :name, from: :fullName
|
|
48
|
+
# end
|
|
49
|
+
# klass.new(fullName: 'Ada').name # => 'Ada'
|
|
41
50
|
def self.included(base)
|
|
42
51
|
base.instance_variable_set(:@transforms, {})
|
|
43
52
|
base.instance_variable_set(:@translations_hash, ::Hash.new { |hash, key| hash[key] = {} })
|
|
44
53
|
base.extend(ClassMethods)
|
|
45
54
|
base.send(:include, InstanceMethods)
|
|
55
|
+
base.extend Hashie::Extensions::DeepMerge
|
|
46
56
|
end
|
|
47
57
|
|
|
58
|
+
# Defines input translations and value transformations.
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# klass = Class.new(Hashie::Dash) do
|
|
62
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
63
|
+
# property :name, from: :fullName
|
|
64
|
+
# end
|
|
65
|
+
# klass.translations # => {fullName: :name}
|
|
48
66
|
module ClassMethods
|
|
67
|
+
# @example Inspect value transformers
|
|
68
|
+
# klass = Class.new(Hashie::Dash) do
|
|
69
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
70
|
+
# property :age, transform_with: ->(value) { value.to_i }
|
|
71
|
+
# end
|
|
72
|
+
# klass.transforms[:age].call('37') # => 37
|
|
73
|
+
# klass.translations_hash # => {}
|
|
49
74
|
attr_reader :transforms, :translations_hash
|
|
50
75
|
|
|
51
76
|
# Ensures that any inheriting classes maintain their translations.
|
|
52
77
|
#
|
|
53
|
-
# *
|
|
78
|
+
# * `:default` - The class inheriting the translations.
|
|
79
|
+
#
|
|
80
|
+
# @example Copy translation settings to subclasses
|
|
81
|
+
# parent = Class.new(Hashie::Dash) do
|
|
82
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
83
|
+
# property :name, from: :fullName
|
|
84
|
+
# end
|
|
85
|
+
# child = Class.new(parent) { property :age, from: :years }
|
|
86
|
+
# child.new(fullName: 'Ada', years: 37).age # => 37
|
|
87
|
+
# parent.translations # => {fullName: :name}
|
|
54
88
|
def inherited(klass)
|
|
55
89
|
super
|
|
56
90
|
klass.instance_variable_set(:@transforms, transforms.dup)
|
|
57
|
-
klass.instance_variable_set(:@translations_hash, translations_hash
|
|
91
|
+
klass.instance_variable_set(:@translations_hash, _deep_dup(translations_hash))
|
|
58
92
|
end
|
|
59
93
|
|
|
94
|
+
# @example List translated input keys alongside untranslated properties
|
|
95
|
+
# klass = Class.new(Hashie::Dash) do
|
|
96
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
97
|
+
# property :name, from: :fullName
|
|
98
|
+
# property :age
|
|
99
|
+
# end
|
|
100
|
+
# klass.permitted_input_keys # => [:fullName, :age]
|
|
60
101
|
def permitted_input_keys
|
|
61
102
|
@permitted_input_keys ||=
|
|
62
103
|
properties
|
|
@@ -65,12 +106,19 @@ module Hashie
|
|
|
65
106
|
|
|
66
107
|
# Defines a property on the Trash. Options are as follows:
|
|
67
108
|
#
|
|
68
|
-
# *
|
|
109
|
+
# * `:default` - Specify a default value for this property, to be
|
|
69
110
|
# returned before a value is set on the property in a new Dash.
|
|
70
|
-
# *
|
|
71
|
-
# *
|
|
72
|
-
# *
|
|
111
|
+
# * `:from` - Specify the original key name that will be write only.
|
|
112
|
+
# * `:with` - Specify a lambda to be used to convert value.
|
|
113
|
+
# * `:transform_with` - Specify a lambda to be used to convert value
|
|
73
114
|
# without using the :from option. It transform the property itself.
|
|
115
|
+
#
|
|
116
|
+
# @example Declare a translated and converted property
|
|
117
|
+
# klass = Class.new(Hashie::Dash) do
|
|
118
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
119
|
+
# property :age, from: :years, with: ->(value) { value.to_i }
|
|
120
|
+
# end
|
|
121
|
+
# klass.new(years: '37').age # => 37
|
|
74
122
|
def property(property_name, options = {})
|
|
75
123
|
super
|
|
76
124
|
|
|
@@ -79,7 +127,7 @@ module Hashie
|
|
|
79
127
|
transformer = options[:transform_with]
|
|
80
128
|
|
|
81
129
|
if from
|
|
82
|
-
fail_self_transformation_error!(property_name) if property_name == from
|
|
130
|
+
fail_self_transformation_error!(property_name) if property_name.to_s == from.to_s
|
|
83
131
|
define_translation(from, property_name, converter || transformer)
|
|
84
132
|
define_writer_for_source_property(from)
|
|
85
133
|
elsif valid_transformer?(transformer)
|
|
@@ -87,18 +135,44 @@ module Hashie
|
|
|
87
135
|
end
|
|
88
136
|
end
|
|
89
137
|
|
|
138
|
+
# @example Apply a declared transformer directly
|
|
139
|
+
# klass = Class.new(Hashie::Dash) do
|
|
140
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
141
|
+
# property :age, transform_with: ->(value) { value.to_i }
|
|
142
|
+
# end
|
|
143
|
+
# klass.transformed_property(:age, '37') # => 37
|
|
90
144
|
def transformed_property(property_name, value)
|
|
91
145
|
transforms[property_name].call(value)
|
|
92
146
|
end
|
|
93
147
|
|
|
148
|
+
# @example Check for a property transformer
|
|
149
|
+
# klass = Class.new(Hashie::Dash) do
|
|
150
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
151
|
+
# property :age, transform_with: ->(value) { value.to_i }
|
|
152
|
+
# end
|
|
153
|
+
# klass.transformation_exists?(:age) # => true
|
|
154
|
+
# klass.transformation_exists?(:name) # => false
|
|
94
155
|
def transformation_exists?(name)
|
|
95
156
|
transforms.key? name
|
|
96
157
|
end
|
|
97
158
|
|
|
159
|
+
# @example Check for a translated source key
|
|
160
|
+
# klass = Class.new(Hashie::Dash) do
|
|
161
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
162
|
+
# property :name, from: :fullName
|
|
163
|
+
# end
|
|
164
|
+
# klass.translation_exists?(:fullName) # => true
|
|
165
|
+
# klass.translation_exists?(:name) # => false
|
|
98
166
|
def translation_exists?(name)
|
|
99
167
|
translations_hash.key? name
|
|
100
168
|
end
|
|
101
169
|
|
|
170
|
+
# @example Map source keys to destination properties
|
|
171
|
+
# klass = Class.new(Hashie::Dash) do
|
|
172
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
173
|
+
# property :name, from: :fullName
|
|
174
|
+
# end
|
|
175
|
+
# klass.translations # => {fullName: :name}
|
|
102
176
|
def translations
|
|
103
177
|
@translations ||= {}.tap do |translations|
|
|
104
178
|
translations_hash.each do |(property_name, property_translations)|
|
|
@@ -112,6 +186,12 @@ module Hashie
|
|
|
112
186
|
end
|
|
113
187
|
end
|
|
114
188
|
|
|
189
|
+
# @example Map destination properties back to source keys
|
|
190
|
+
# klass = Class.new(Hashie::Dash) do
|
|
191
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
192
|
+
# property :name, from: :fullName
|
|
193
|
+
# end
|
|
194
|
+
# klass.inverse_translations # => {name: :fullName}
|
|
115
195
|
def inverse_translations
|
|
116
196
|
@inverse_translations ||= {}.tap do |translations|
|
|
117
197
|
translations_hash.each do |(property_name, property_translations)|
|
|
@@ -138,7 +218,7 @@ module Hashie
|
|
|
138
218
|
|
|
139
219
|
def fail_self_transformation_error!(property_name)
|
|
140
220
|
raise ArgumentError,
|
|
141
|
-
"
|
|
221
|
+
"property name `#{property_name}` and :from option must not be the same"
|
|
142
222
|
end
|
|
143
223
|
|
|
144
224
|
def valid_transformer?(transformer)
|
|
@@ -146,39 +226,77 @@ module Hashie
|
|
|
146
226
|
end
|
|
147
227
|
end
|
|
148
228
|
|
|
229
|
+
# Applies translations and transformations during assignment.
|
|
230
|
+
#
|
|
231
|
+
# @example
|
|
232
|
+
# klass = Class.new(Hashie::Dash) do
|
|
233
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
234
|
+
# property :name, from: :fullName
|
|
235
|
+
# end
|
|
236
|
+
# klass.new(fullName: 'Ada').name # => 'Ada'
|
|
149
237
|
module InstanceMethods
|
|
150
238
|
# Sets a value on the Dash in a Hash-like way.
|
|
151
239
|
#
|
|
152
240
|
# Note: Only works on pre-existing properties.
|
|
241
|
+
#
|
|
242
|
+
# @example Assign through a translated source key
|
|
243
|
+
# klass = Class.new(Hashie::Dash) do
|
|
244
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
245
|
+
# property :name, from: :fullName
|
|
246
|
+
# end
|
|
247
|
+
# dash = klass.new
|
|
248
|
+
# dash[:fullName] = 'Ada'
|
|
249
|
+
# dash.name # => 'Ada'
|
|
153
250
|
def []=(property, value)
|
|
154
251
|
if self.class.translation_exists? property
|
|
155
252
|
send("#{property}=", value)
|
|
156
253
|
|
|
157
254
|
if self.class.transformation_exists? property
|
|
158
|
-
super
|
|
255
|
+
super(property, self.class.transformed_property(property, value))
|
|
159
256
|
elsif self.class.properties.include?(property)
|
|
160
|
-
super
|
|
257
|
+
super
|
|
161
258
|
end
|
|
162
259
|
elsif self.class.transformation_exists? property
|
|
163
|
-
super
|
|
260
|
+
super(property, self.class.transformed_property(property, value))
|
|
164
261
|
elsif property_exists? property
|
|
165
262
|
super
|
|
166
263
|
end
|
|
167
264
|
end
|
|
168
265
|
|
|
169
266
|
# Deletes any keys that have a translation
|
|
267
|
+
#
|
|
268
|
+
# @example Initialize translated input without modifying the source
|
|
269
|
+
# klass = Class.new(Hashie::Dash) do
|
|
270
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
271
|
+
# property :name, from: :fullName
|
|
272
|
+
# end
|
|
273
|
+
# attributes = {fullName: 'Ada'}
|
|
274
|
+
# dash = klass.new
|
|
275
|
+
# dash.initialize_attributes(attributes)
|
|
276
|
+
# dash.name # => 'Ada'
|
|
277
|
+
# attributes # => {fullName: 'Ada'}
|
|
170
278
|
def initialize_attributes(attributes)
|
|
171
279
|
return unless attributes
|
|
280
|
+
|
|
172
281
|
attributes_copy = attributes.dup.delete_if do |k, v|
|
|
173
282
|
if self.class.translations_hash.include?(k)
|
|
174
283
|
self[k] = v
|
|
175
284
|
true
|
|
176
285
|
end
|
|
177
286
|
end
|
|
178
|
-
super
|
|
287
|
+
super(attributes_copy)
|
|
179
288
|
end
|
|
180
289
|
|
|
181
290
|
# Raises an NoMethodError if the property doesn't exist
|
|
291
|
+
#
|
|
292
|
+
# @example Check for an existing property
|
|
293
|
+
# klass = Class.new(Hashie::Dash) do
|
|
294
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
295
|
+
# property :name
|
|
296
|
+
# end
|
|
297
|
+
# dash = klass.new
|
|
298
|
+
# dash.property_exists?(:name) # => true
|
|
299
|
+
# expect { dash.property_exists?(:missing) }.to raise_error(NoMethodError)
|
|
182
300
|
def property_exists?(property)
|
|
183
301
|
fail_no_property_error!(property) unless self.class.property?(property)
|
|
184
302
|
true
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
|
-
# Searches a deeply nested
|
|
5
|
+
# Searches a deeply nested data structure for a key path, and returns the associated value.
|
|
4
6
|
#
|
|
5
7
|
# options = { user: { location: { address: '123 Street' } } }
|
|
6
8
|
# options.deep_fetch :user, :location, :address #=> '123 Street'
|
|
@@ -11,19 +13,29 @@ module Hashie
|
|
|
11
13
|
#
|
|
12
14
|
# This is particularly useful for fetching values from deeply nested api responses
|
|
13
15
|
# or params hashes.
|
|
16
|
+
#
|
|
17
|
+
# @example Traverse hashes and arrays
|
|
18
|
+
# hash = { users: [{ name: 'Ada' }] }.extend(Hashie::Extensions::DeepFetch)
|
|
19
|
+
# hash.deep_fetch(:users, 0, :name) # => 'Ada'
|
|
14
20
|
module DeepFetch
|
|
21
|
+
# @example Report an undefined path
|
|
22
|
+
# error = Hashie::Extensions::DeepFetch::UndefinedPathError.new('missing address')
|
|
23
|
+
# error.message # => 'missing address'
|
|
15
24
|
class UndefinedPathError < StandardError; end
|
|
16
25
|
|
|
26
|
+
# @example Use a fallback for a missing path
|
|
27
|
+
# hash = { user: {} }.extend(Hashie::Extensions::DeepFetch)
|
|
28
|
+
# hash.deep_fetch(:user, :name) { |key| "missing #{key}" } # => 'missing name'
|
|
29
|
+
# hash.deep_fetch(:user) # => {}
|
|
17
30
|
def deep_fetch(*args, &block)
|
|
18
31
|
args.reduce(self) do |obj, arg|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
32
|
+
arg = Integer(arg) if obj.is_a? Array
|
|
33
|
+
obj.fetch(arg)
|
|
34
|
+
rescue ArgumentError, IndexError, NoMethodError => e
|
|
35
|
+
break yield(arg) if block
|
|
36
|
+
|
|
37
|
+
raise UndefinedPathError,
|
|
38
|
+
"could not fetch path (`#{args.join(' > ')}`) at `#{arg}`", e.backtrace
|
|
27
39
|
end
|
|
28
40
|
end
|
|
29
41
|
end
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'hashie/extensions/deep_locate'
|
|
2
4
|
module Hashie
|
|
3
5
|
module Extensions
|
|
6
|
+
# @example Find values in nested collections
|
|
7
|
+
# hash = { users: [{ name: 'Ada' }] }.extend(Hashie::Extensions::DeepFind)
|
|
8
|
+
# hash.deep_find(:name) # => 'Ada'
|
|
4
9
|
module DeepFind
|
|
5
10
|
# Performs a depth-first search on deeply nested data structures for
|
|
6
11
|
# a key and returns the first occurrence of the key.
|
|
@@ -16,10 +21,17 @@ module Hashie
|
|
|
16
21
|
# my_hash = MyHash.new
|
|
17
22
|
# my_hash[:user] = {location: {address: '123 Street'}}
|
|
18
23
|
# my_hash.deep_find(:address) # => '123 Street'
|
|
24
|
+
#
|
|
25
|
+
# @example Return the first value
|
|
26
|
+
# hash = { users: [{ name: 'Ada' }, { name: 'Grace' }] }.extend(Hashie::Extensions::DeepFind)
|
|
27
|
+
# hash.deep_find(:name) # => 'Ada'
|
|
19
28
|
def deep_find(key)
|
|
20
29
|
_deep_find(key)
|
|
21
30
|
end
|
|
22
31
|
|
|
32
|
+
# @example Detect the first nested value
|
|
33
|
+
# hash = { user: { name: 'Ada' } }.extend(Hashie::Extensions::DeepFind)
|
|
34
|
+
# hash.deep_detect(:name) # => 'Ada'
|
|
23
35
|
alias deep_detect deep_find
|
|
24
36
|
|
|
25
37
|
# Performs a depth-first search on deeply nested data structures for
|
|
@@ -44,11 +56,19 @@ module Hashie
|
|
|
44
56
|
# {location: {address: '234 Street'}}
|
|
45
57
|
# ]
|
|
46
58
|
# my_hash.deep_find_all(:address) # => ['123 Street', '234 Street']
|
|
59
|
+
#
|
|
60
|
+
# @example Collect all values, or nil if absent
|
|
61
|
+
# hash = { users: [{ name: 'Ada' }, { name: 'Grace' }] }.extend(Hashie::Extensions::DeepFind)
|
|
62
|
+
# hash.deep_find_all(:name) # => ['Ada', 'Grace']
|
|
63
|
+
# hash.deep_find_all(:email) # => nil
|
|
47
64
|
def deep_find_all(key)
|
|
48
65
|
matches = _deep_find_all(key)
|
|
49
66
|
matches.empty? ? nil : matches
|
|
50
67
|
end
|
|
51
68
|
|
|
69
|
+
# @example Select every nested value
|
|
70
|
+
# hash = { users: [{ name: 'Ada' }, { name: 'Grace' }] }.extend(Hashie::Extensions::DeepFind)
|
|
71
|
+
# hash.deep_select(:name) # => ['Ada', 'Grace']
|
|
52
72
|
alias deep_select deep_find_all
|
|
53
73
|
|
|
54
74
|
private
|