hashie 4.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -53
  3. data/CONTRIBUTING.md +38 -1
  4. data/LICENSE +1 -1
  5. data/README.md +920 -205
  6. data/Rakefile +32 -1
  7. data/UPGRADING.md +56 -0
  8. data/hashie.gemspec +10 -5
  9. data/lib/hashie/array.rb +15 -12
  10. data/lib/hashie/clash.rb +59 -4
  11. data/lib/hashie/dash.rb +160 -32
  12. data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
  13. data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
  14. data/lib/hashie/extensions/block_access.rb +71 -0
  15. data/lib/hashie/extensions/coercion.rb +180 -26
  16. data/lib/hashie/extensions/dash/coercion.rb +34 -3
  17. data/lib/hashie/extensions/dash/indifferent_access.rb +90 -3
  18. data/lib/hashie/extensions/dash/lazy.rb +170 -0
  19. data/lib/hashie/extensions/dash/predefined_values.rb +148 -0
  20. data/lib/hashie/extensions/dash/property_translation.rb +138 -15
  21. data/lib/hashie/extensions/deep_fetch.rb +21 -9
  22. data/lib/hashie/extensions/deep_find.rb +20 -0
  23. data/lib/hashie/extensions/deep_grep.rb +69 -0
  24. data/lib/hashie/extensions/deep_locate.rb +16 -29
  25. data/lib/hashie/extensions/deep_merge.rb +16 -0
  26. data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
  27. data/lib/hashie/extensions/ignore_undeclared.rb +24 -7
  28. data/lib/hashie/extensions/indifferent_access.rb +124 -5
  29. data/lib/hashie/extensions/kernel_coercion.rb +58 -0
  30. data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
  31. data/lib/hashie/extensions/key_conversion.rb +9 -0
  32. data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
  33. data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
  34. data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
  35. data/lib/hashie/extensions/mash/required_keys.rb +111 -0
  36. data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
  37. data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
  38. data/lib/hashie/extensions/mash/symbolize_keys.rb +11 -7
  39. data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
  40. data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
  41. data/lib/hashie/extensions/merge_initializer.rb +9 -0
  42. data/lib/hashie/extensions/method_access.rb +94 -8
  43. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
  44. data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
  45. data/lib/hashie/extensions/persistable.rb +137 -0
  46. data/lib/hashie/extensions/pretty_inspect.rb +14 -0
  47. data/lib/hashie/extensions/ruby_version.rb +18 -1
  48. data/lib/hashie/extensions/ruby_version_check.rb +17 -0
  49. data/lib/hashie/extensions/strict_key_access.rb +59 -5
  50. data/lib/hashie/extensions/stringify_keys.rb +72 -17
  51. data/lib/hashie/extensions/symbolize_keys.rb +84 -17
  52. data/lib/hashie/hash.rb +18 -2
  53. data/lib/hashie/logger.rb +18 -1
  54. data/lib/hashie/mash.rb +314 -82
  55. data/lib/hashie/railtie.rb +27 -1
  56. data/lib/hashie/rash.rb +64 -4
  57. data/lib/hashie/trash.rb +8 -0
  58. data/lib/hashie/utils.rb +17 -6
  59. data/lib/hashie/version.rb +3 -1
  60. data/lib/hashie.rb +46 -0
  61. metadata +22 -9
@@ -1,12 +1,33 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hashie
2
4
  module Extensions
3
5
  module Dash
6
+ # Allows string and symbol access to declared Dash properties.
7
+ #
8
+ # @example
9
+ # klass = Class.new(Hashie::Dash) do
10
+ # include Hashie::Extensions::Dash::IndifferentAccess
11
+ # property :name
12
+ # end
13
+ # klass.new(name: 'Ada')['name'] # => 'Ada'
4
14
  module IndifferentAccess
15
+ # @example Include indifferent access in a Dash subclass
16
+ # klass = Class.new(Hashie::Dash) do
17
+ # include Hashie::Extensions::Dash::IndifferentAccess
18
+ # property :name
19
+ # end
20
+ # klass.property?('name') # => true
5
21
  def self.included(base)
6
22
  base.extend ClassMethods
7
23
  base.send :include, Hashie::Extensions::IndifferentAccess
8
24
  end
9
25
 
26
+ # @example Add Dash-specific class methods when needed
27
+ # klass = Class.new(Hashie::Dash) { property :name }
28
+ # Hashie::Extensions::Dash::IndifferentAccess.maybe_extend(klass)
29
+ # klass.property?('name') # => true
30
+ # klass.singleton_class.included_modules.include?(Hashie::Extensions::Dash::IndifferentAccess::ClassMethods) # => true
10
31
  def self.maybe_extend(base)
11
32
  return unless requires_class_methods?(base)
12
33
 
@@ -19,25 +40,91 @@ module Hashie
19
40
  end
20
41
  private_class_method :requires_class_methods?
21
42
 
43
+ # @example Include defaults under string keys in the returned hash
44
+ # klass = Class.new(Hashie::Dash) do
45
+ # include Hashie::Extensions::Dash::IndifferentAccess
46
+ # property :name, default: 'Ada'
47
+ # property :age
48
+ # end
49
+ # klass.new(age: 37).to_h # => {'name' => 'Ada', 'age' => 37}
50
+ def to_h
51
+ defaults = self.class.properties.to_h do |prop|
52
+ [Hashie::Extensions::IndifferentAccess.convert_key(prop), self.class.defaults[prop]]
53
+ end
54
+
55
+ defaults.merge(self)
56
+ end
57
+ # @example
58
+ # klass = Class.new(Hashie::Dash) do
59
+ # include Hashie::Extensions::Dash::IndifferentAccess
60
+ # property :name, default: 'Ada'
61
+ # end
62
+ # klass.new.to_hash # => {'name' => 'Ada'}
63
+ alias to_hash to_h
64
+
65
+ # Class-level property and translation lookup with indifferent keys.
66
+ #
67
+ # @example
68
+ # klass = Class.new(Hashie::Dash) do
69
+ # include Hashie::Extensions::Dash::IndifferentAccess
70
+ # property :name
71
+ # end
72
+ # klass.property?('name') # => true
22
73
  module ClassMethods
23
74
  # Check to see if the specified property has already been
24
75
  # defined.
76
+ #
77
+ # @example
78
+ # klass = Class.new(Hashie::Dash) do
79
+ # include Hashie::Extensions::Dash::IndifferentAccess
80
+ # property :name
81
+ # end
82
+ # klass.property?('name') # => true
83
+ # klass.property?(:missing) # => false
25
84
  def property?(name)
26
- name = translations[name.to_sym] if translation_for?(name)
27
- name = name.to_s
28
- !!properties.find { |property| property.to_s == name }
85
+ targets = if translation_for?(name)
86
+ Array(translations.find { |key, _| key.to_s == name.to_s }.last)
87
+ else
88
+ [name]
89
+ end
90
+ targets.all? do |target|
91
+ properties.any? { |property| property.to_s == target.to_s }
92
+ end
29
93
  end
30
94
 
95
+ # @example Look up a translation with a string or symbol
96
+ # klass = Class.new(Hashie::Dash) do
97
+ # include Hashie::Extensions::Dash::PropertyTranslation
98
+ # include Hashie::Extensions::Dash::IndifferentAccess
99
+ # property :name, from: :fullName
100
+ # end
101
+ # klass.translation_exists?('fullName') # => true
102
+ # klass.translation_exists?(:missing) # => false
31
103
  def translation_exists?(name)
32
104
  name = name.to_s
33
105
  !!translations.keys.find { |key| key.to_s == name }
34
106
  end
35
107
 
108
+ # @example Apply a transformer declared with a symbol key
109
+ # klass = Class.new(Hashie::Dash) do
110
+ # include Hashie::Extensions::Dash::PropertyTranslation
111
+ # include Hashie::Extensions::Dash::IndifferentAccess
112
+ # property :age, transform_with: ->(value) { value.to_i }
113
+ # end
114
+ # klass.transformed_property('age', '37') # => 37
36
115
  def transformed_property(property_name, value)
37
116
  transform = transforms[property_name] || transforms[property_name.to_sym]
38
117
  transform.call(value)
39
118
  end
40
119
 
120
+ # @example Find a transformer regardless of key type
121
+ # klass = Class.new(Hashie::Dash) do
122
+ # include Hashie::Extensions::Dash::PropertyTranslation
123
+ # include Hashie::Extensions::Dash::IndifferentAccess
124
+ # property :age, transform_with: ->(value) { value.to_i }
125
+ # end
126
+ # klass.transformation_exists?('age') # => true
127
+ # klass.transformation_exists?(:missing) # => false
41
128
  def transformation_exists?(name)
42
129
  name = name.to_s
43
130
  !!transforms.keys.find { |key| key.to_s == name }
@@ -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
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hashie
4
+ module Extensions
5
+ module Dash
6
+ # Extends a Dash with the ability to accept only predefined values on a property.
7
+ #
8
+ # @example
9
+ #
10
+ # class PersonHash < Hashie::Dash
11
+ # include Hashie::Extensions::Dash::PredefinedValues
12
+ #
13
+ # property :gender, values: [:male, :female, :prefer_not_to_say]
14
+ # property :age, values: (0..150) # a Range
15
+ # end
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
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 # => {}
26
+ def self.included(base)
27
+ base.instance_variable_set(:@values_for_properties, {})
28
+ base.extend(ClassMethods)
29
+ base.include(InstanceMethods)
30
+ end
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]}
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)}
47
+ attr_reader :values_for_properties
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]
57
+ def inherited(klass)
58
+ super
59
+ klass.instance_variable_set(:@values_for_properties, values_for_properties.dup)
60
+ end
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/)
70
+ def property(property_name, options = {})
71
+ super
72
+
73
+ return unless (predefined_values = options[:values])
74
+
75
+ assert_predefined_values!(predefined_values)
76
+ set_predefined_values(property_name, predefined_values)
77
+ end
78
+
79
+ private
80
+
81
+ def assert_predefined_values!(predefined_values)
82
+ return if supported_type?(predefined_values)
83
+
84
+ raise ArgumentError, %(`values` accepts an `Array` or a `Range`)
85
+ end
86
+
87
+ def supported_type?(predefined_values)
88
+ [::Array, ::Range].any? { |klass| predefined_values.is_a?(klass) }
89
+ end
90
+
91
+ def set_predefined_values(property_name, predefined_values)
92
+ @values_for_properties[property_name] = predefined_values
93
+ end
94
+ end
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
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
112
+ def initialize(*)
113
+ super
114
+
115
+ assert_property_values!
116
+ end
117
+
118
+ private
119
+
120
+ def assert_property_values!
121
+ self.class.values_for_properties.each_key do |property|
122
+ value = send(property)
123
+
124
+ allowed_values = values_for_properties(property)
125
+ fail_property_value_error!(property, value, allowed_values) if value && !allowed_values.include?(value)
126
+ end
127
+ end
128
+
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}"
139
+ end
140
+
141
+ def values_for_properties(property)
142
+ self.class.values_for_properties[property]
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end