hashie 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -0
  3. data/CONTRIBUTING.md +38 -1
  4. data/README.md +816 -181
  5. data/Rakefile +29 -1
  6. data/UPGRADING.md +18 -0
  7. data/hashie.gemspec +8 -7
  8. data/lib/hashie/array.rb +15 -12
  9. data/lib/hashie/clash.rb +59 -4
  10. data/lib/hashie/dash.rb +144 -16
  11. data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
  12. data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
  13. data/lib/hashie/extensions/block_access.rb +71 -0
  14. data/lib/hashie/extensions/coercion.rb +180 -19
  15. data/lib/hashie/extensions/dash/coercion.rb +34 -3
  16. data/lib/hashie/extensions/dash/indifferent_access.rb +83 -5
  17. data/lib/hashie/extensions/dash/lazy.rb +170 -0
  18. data/lib/hashie/extensions/dash/predefined_values.rb +69 -9
  19. data/lib/hashie/extensions/dash/property_translation.rb +134 -16
  20. data/lib/hashie/extensions/deep_fetch.rb +21 -9
  21. data/lib/hashie/extensions/deep_find.rb +20 -0
  22. data/lib/hashie/extensions/deep_grep.rb +69 -0
  23. data/lib/hashie/extensions/deep_locate.rb +15 -28
  24. data/lib/hashie/extensions/deep_merge.rb +16 -0
  25. data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
  26. data/lib/hashie/extensions/ignore_undeclared.rb +21 -3
  27. data/lib/hashie/extensions/indifferent_access.rb +100 -14
  28. data/lib/hashie/extensions/kernel_coercion.rb +58 -0
  29. data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
  30. data/lib/hashie/extensions/key_conversion.rb +9 -0
  31. data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
  32. data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
  33. data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
  34. data/lib/hashie/extensions/mash/required_keys.rb +111 -0
  35. data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
  36. data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
  37. data/lib/hashie/extensions/mash/symbolize_keys.rb +6 -2
  38. data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
  39. data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
  40. data/lib/hashie/extensions/merge_initializer.rb +9 -0
  41. data/lib/hashie/extensions/method_access.rb +93 -7
  42. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
  43. data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
  44. data/lib/hashie/extensions/persistable.rb +137 -0
  45. data/lib/hashie/extensions/pretty_inspect.rb +14 -0
  46. data/lib/hashie/extensions/ruby_version.rb +18 -1
  47. data/lib/hashie/extensions/ruby_version_check.rb +17 -0
  48. data/lib/hashie/extensions/strict_key_access.rb +59 -5
  49. data/lib/hashie/extensions/stringify_keys.rb +72 -17
  50. data/lib/hashie/extensions/symbolize_keys.rb +72 -16
  51. data/lib/hashie/hash.rb +16 -0
  52. data/lib/hashie/logger.rb +18 -1
  53. data/lib/hashie/mash.rb +293 -62
  54. data/lib/hashie/railtie.rb +27 -1
  55. data/lib/hashie/rash.rb +64 -4
  56. data/lib/hashie/trash.rb +8 -0
  57. data/lib/hashie/utils.rb +17 -6
  58. data/lib/hashie/version.rb +3 -1
  59. data/lib/hashie.rb +45 -0
  60. metadata +15 -17
@@ -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
- # == Example
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
- # person = PersonHash.new(gender: :male, age: -1)
16
- # # => ArgumentError: The value '-1' is not accepted for property 'age'
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
- if value && !values_for_properties(property).include?(value)
72
- fail_property_value_error!(property)
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
- raise ArgumentError, "Invalid value for property '#{property}'"
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
- # == Example from inconsistent APIs
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
- # == Example using translation lambdas
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 (Fixnum if you are using Ruby 2.3 or lower)
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
- # * <tt>:default</tt> - The class inheriting the translations.
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.dup)
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
- # * <tt>:default</tt> - Specify a default value for this property, to be
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
- # * <tt>:from</tt> - Specify the original key name that will be write only.
71
- # * <tt>:with</tt> - Specify a lambda to be used to convert value.
72
- # * <tt>:transform_with</tt> - Specify a lambda to be used to convert value
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
- "Property name (#{property_name}) and :from option must not be the same"
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 property, self.class.transformed_property(property, value)
255
+ super(property, self.class.transformed_property(property, value))
159
256
  elsif self.class.properties.include?(property)
160
- super(property, value)
257
+ super
161
258
  end
162
259
  elsif self.class.transformation_exists? property
163
- super property, self.class.transformed_property(property, value)
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 attributes_copy
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 datastructure for a key path, and returns the associated value.
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
- begin
20
- arg = Integer(arg) if obj.is_a? Array
21
- obj.fetch(arg)
22
- rescue ArgumentError, IndexError, NoMethodError => e
23
- break yield(arg) if block
24
- raise UndefinedPathError,
25
- "Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace
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