hashie 2.1.2 → 4.1.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 (71) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +524 -59
  3. data/CONTRIBUTING.md +24 -7
  4. data/README.md +781 -90
  5. data/Rakefile +19 -2
  6. data/UPGRADING.md +245 -0
  7. data/hashie.gemspec +21 -13
  8. data/lib/hashie.rb +60 -21
  9. data/lib/hashie/array.rb +21 -0
  10. data/lib/hashie/clash.rb +24 -12
  11. data/lib/hashie/dash.rb +96 -33
  12. data/lib/hashie/extensions/active_support/core_ext/hash.rb +14 -0
  13. data/lib/hashie/extensions/array/pretty_inspect.rb +19 -0
  14. data/lib/hashie/extensions/coercion.rb +124 -18
  15. data/lib/hashie/extensions/dash/coercion.rb +25 -0
  16. data/lib/hashie/extensions/dash/indifferent_access.rb +56 -0
  17. data/lib/hashie/extensions/dash/property_translation.rb +191 -0
  18. data/lib/hashie/extensions/deep_fetch.rb +7 -5
  19. data/lib/hashie/extensions/deep_find.rb +69 -0
  20. data/lib/hashie/extensions/deep_locate.rb +113 -0
  21. data/lib/hashie/extensions/deep_merge.rb +35 -12
  22. data/lib/hashie/extensions/ignore_undeclared.rb +11 -5
  23. data/lib/hashie/extensions/indifferent_access.rb +28 -16
  24. data/lib/hashie/extensions/key_conflict_warning.rb +55 -0
  25. data/lib/hashie/extensions/key_conversion.rb +0 -82
  26. data/lib/hashie/extensions/mash/define_accessors.rb +90 -0
  27. data/lib/hashie/extensions/mash/keep_original_keys.rb +53 -0
  28. data/lib/hashie/extensions/mash/permissive_respond_to.rb +61 -0
  29. data/lib/hashie/extensions/mash/safe_assignment.rb +18 -0
  30. data/lib/hashie/extensions/mash/symbolize_keys.rb +38 -0
  31. data/lib/hashie/extensions/method_access.rb +154 -11
  32. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +48 -0
  33. data/lib/hashie/extensions/pretty_inspect.rb +19 -0
  34. data/lib/hashie/extensions/ruby_version.rb +60 -0
  35. data/lib/hashie/extensions/ruby_version_check.rb +21 -0
  36. data/lib/hashie/extensions/strict_key_access.rb +77 -0
  37. data/lib/hashie/extensions/stringify_keys.rb +71 -0
  38. data/lib/hashie/extensions/symbolize_keys.rb +71 -0
  39. data/lib/hashie/hash.rb +27 -8
  40. data/lib/hashie/logger.rb +18 -0
  41. data/lib/hashie/mash.rb +235 -57
  42. data/lib/hashie/railtie.rb +21 -0
  43. data/lib/hashie/rash.rb +40 -16
  44. data/lib/hashie/trash.rb +2 -88
  45. data/lib/hashie/utils.rb +44 -0
  46. data/lib/hashie/version.rb +1 -1
  47. metadata +42 -81
  48. data/.gitignore +0 -9
  49. data/.rspec +0 -2
  50. data/.rubocop.yml +0 -36
  51. data/.travis.yml +0 -15
  52. data/Gemfile +0 -11
  53. data/Guardfile +0 -5
  54. data/lib/hashie/hash_extensions.rb +0 -47
  55. data/spec/hashie/clash_spec.rb +0 -48
  56. data/spec/hashie/dash_spec.rb +0 -338
  57. data/spec/hashie/extensions/coercion_spec.rb +0 -156
  58. data/spec/hashie/extensions/deep_fetch_spec.rb +0 -70
  59. data/spec/hashie/extensions/deep_merge_spec.rb +0 -22
  60. data/spec/hashie/extensions/ignore_undeclared_spec.rb +0 -23
  61. data/spec/hashie/extensions/indifferent_access_spec.rb +0 -152
  62. data/spec/hashie/extensions/key_conversion_spec.rb +0 -103
  63. data/spec/hashie/extensions/merge_initializer_spec.rb +0 -23
  64. data/spec/hashie/extensions/method_access_spec.rb +0 -121
  65. data/spec/hashie/hash_spec.rb +0 -66
  66. data/spec/hashie/mash_spec.rb +0 -467
  67. data/spec/hashie/rash_spec.rb +0 -44
  68. data/spec/hashie/trash_spec.rb +0 -193
  69. data/spec/hashie/version_spec.rb +0 -7
  70. data/spec/spec.opts +0 -3
  71. data/spec/spec_helper.rb +0 -8
@@ -0,0 +1,25 @@
1
+ module Hashie
2
+ module Extensions
3
+ module Dash
4
+ module Coercion
5
+ # Extends a Dash with the ability to define coercion for properties.
6
+
7
+ def self.included(base)
8
+ base.send :include, Hashie::Extensions::Coercion
9
+ base.extend ClassMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ # Defines a property on the Dash. Options are the standard
14
+ # <tt>Hashie::Dash#property</tt> options plus:
15
+ #
16
+ # * <tt>:coerce</tt> - The class into which you want the property coerced.
17
+ def property(property_name, options = {})
18
+ super
19
+ coerce_key property_name, options[:coerce] if options[:coerce]
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,56 @@
1
+ module Hashie
2
+ module Extensions
3
+ module Dash
4
+ module IndifferentAccess
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ base.send :include, Hashie::Extensions::IndifferentAccess
8
+ end
9
+
10
+ def self.maybe_extend(base)
11
+ return unless requires_class_methods?(base)
12
+
13
+ base.extend(ClassMethods)
14
+ end
15
+
16
+ def self.requires_class_methods?(klass)
17
+ klass <= Hashie::Dash &&
18
+ !klass.singleton_class.included_modules.include?(ClassMethods)
19
+ end
20
+ private_class_method :requires_class_methods?
21
+
22
+ module ClassMethods
23
+ # Check to see if the specified property has already been
24
+ # defined.
25
+ 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 }
29
+ end
30
+
31
+ def translation_exists?(name)
32
+ name = name.to_s
33
+ !!translations.keys.find { |key| key.to_s == name }
34
+ end
35
+
36
+ def transformed_property(property_name, value)
37
+ transform = transforms[property_name] || transforms[property_name.to_sym]
38
+ transform.call(value)
39
+ end
40
+
41
+ def transformation_exists?(name)
42
+ name = name.to_s
43
+ !!transforms.keys.find { |key| key.to_s == name }
44
+ end
45
+
46
+ private
47
+
48
+ def translation_for?(name)
49
+ included_modules.include?(Hashie::Extensions::Dash::PropertyTranslation) &&
50
+ translation_exists?(name)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,191 @@
1
+ module Hashie
2
+ module Extensions
3
+ module Dash
4
+ # Extends a Dash with the ability to remap keys from a source hash.
5
+ #
6
+ # Property translation is useful when you need to read data from another
7
+ # application -- such as a Java API -- where the keys are named
8
+ # differently from Ruby conventions.
9
+ #
10
+ # == Example from inconsistent APIs
11
+ #
12
+ # class PersonHash < Hashie::Dash
13
+ # include Hashie::Extensions::Dash::PropertyTranslation
14
+ #
15
+ # property :first_name, from :firstName
16
+ # property :last_name, from: :lastName
17
+ # property :first_name, from: :f_name
18
+ # property :last_name, from: :l_name
19
+ # end
20
+ #
21
+ # person = PersonHash.new(firstName: 'Michael', l_name: 'Bleigh')
22
+ # person[:first_name] #=> 'Michael'
23
+ # person[:last_name] #=> 'Bleigh'
24
+ #
25
+ # You can also use a lambda to translate the value. This is particularly
26
+ # useful when you want to ensure the type of data you're wrapping.
27
+ #
28
+ # == Example using translation lambdas
29
+ #
30
+ # class DataModelHash < Hashie::Dash
31
+ # include Hashie::Extensions::Dash::PropertyTranslation
32
+ #
33
+ # property :id, transform_with: ->(value) { value.to_i }
34
+ # property :created_at, from: :created, with: ->(value) { Time.parse(value) }
35
+ # end
36
+ #
37
+ # 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)
39
+ # model.created_at.class #=> Time
40
+ module PropertyTranslation
41
+ def self.included(base)
42
+ base.instance_variable_set(:@transforms, {})
43
+ base.instance_variable_set(:@translations_hash, ::Hash.new { |hash, key| hash[key] = {} })
44
+ base.extend(ClassMethods)
45
+ base.send(:include, InstanceMethods)
46
+ end
47
+
48
+ module ClassMethods
49
+ attr_reader :transforms, :translations_hash
50
+
51
+ # Ensures that any inheriting classes maintain their translations.
52
+ #
53
+ # * <tt>:default</tt> - The class inheriting the translations.
54
+ def inherited(klass)
55
+ super
56
+ klass.instance_variable_set(:@transforms, transforms.dup)
57
+ klass.instance_variable_set(:@translations_hash, translations_hash.dup)
58
+ end
59
+
60
+ def permitted_input_keys
61
+ @permitted_input_keys ||=
62
+ properties
63
+ .map { |property| inverse_translations.fetch property, property }
64
+ end
65
+
66
+ # Defines a property on the Trash. Options are as follows:
67
+ #
68
+ # * <tt>:default</tt> - Specify a default value for this property, to be
69
+ # 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
73
+ # without using the :from option. It transform the property itself.
74
+ def property(property_name, options = {})
75
+ super
76
+
77
+ from = options[:from]
78
+ converter = options[:with]
79
+ transformer = options[:transform_with]
80
+
81
+ if from
82
+ fail_self_transformation_error!(property_name) if property_name == from
83
+ define_translation(from, property_name, converter || transformer)
84
+ define_writer_for_source_property(from)
85
+ elsif valid_transformer?(transformer)
86
+ transforms[property_name] = transformer
87
+ end
88
+ end
89
+
90
+ def transformed_property(property_name, value)
91
+ transforms[property_name].call(value)
92
+ end
93
+
94
+ def transformation_exists?(name)
95
+ transforms.key? name
96
+ end
97
+
98
+ def translation_exists?(name)
99
+ translations_hash.key? name
100
+ end
101
+
102
+ def translations
103
+ @translations ||= {}.tap do |translations|
104
+ translations_hash.each do |(property_name, property_translations)|
105
+ translations[property_name] =
106
+ if property_translations.size > 1
107
+ property_translations.keys
108
+ else
109
+ property_translations.keys.first
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ def inverse_translations
116
+ @inverse_translations ||= {}.tap do |translations|
117
+ translations_hash.each do |(property_name, property_translations)|
118
+ property_translations.each_key do |key|
119
+ translations[key] = property_name
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ def define_translation(from, property_name, translator)
128
+ translations_hash[from][property_name] = translator
129
+ end
130
+
131
+ def define_writer_for_source_property(property)
132
+ define_method "#{property}=" do |val|
133
+ __translations[property].each do |name, with|
134
+ self[name] = with.respond_to?(:call) ? with.call(val) : val
135
+ end
136
+ end
137
+ end
138
+
139
+ def fail_self_transformation_error!(property_name)
140
+ raise ArgumentError,
141
+ "Property name (#{property_name}) and :from option must not be the same"
142
+ end
143
+
144
+ def valid_transformer?(transformer)
145
+ transformer.respond_to? :call
146
+ end
147
+ end
148
+
149
+ module InstanceMethods
150
+ # Sets a value on the Dash in a Hash-like way.
151
+ #
152
+ # Note: Only works on pre-existing properties.
153
+ def []=(property, value)
154
+ if self.class.translation_exists? property
155
+ send("#{property}=", value)
156
+ super(property, value) if self.class.properties.include?(property)
157
+ elsif self.class.transformation_exists? property
158
+ super property, self.class.transformed_property(property, value)
159
+ elsif property_exists? property
160
+ super
161
+ end
162
+ end
163
+
164
+ # Deletes any keys that have a translation
165
+ def initialize_attributes(attributes)
166
+ return unless attributes
167
+ attributes_copy = attributes.dup.delete_if do |k, v|
168
+ if self.class.translations_hash.include?(k)
169
+ self[k] = v
170
+ true
171
+ end
172
+ end
173
+ super attributes_copy
174
+ end
175
+
176
+ # Raises an NoMethodError if the property doesn't exist
177
+ def property_exists?(property)
178
+ fail_no_property_error!(property) unless self.class.property?(property)
179
+ true
180
+ end
181
+
182
+ private
183
+
184
+ def __translations
185
+ self.class.translations_hash
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -9,18 +9,20 @@ module Hashie
9
9
  #
10
10
  # options.deep_fetch(:user, :non_existent_key) { 'a value' } #=> 'a value'
11
11
  #
12
- # This is particularly useful for fetching values from deeply nested api responses or params hashes.
12
+ # This is particularly useful for fetching values from deeply nested api responses
13
+ # or params hashes.
13
14
  module DeepFetch
14
15
  class UndefinedPathError < StandardError; end
15
16
 
16
17
  def deep_fetch(*args, &block)
17
18
  args.reduce(self) do |obj, arg|
18
19
  begin
19
- arg = Integer(arg) if obj.kind_of? Array
20
+ arg = Integer(arg) if obj.is_a? Array
20
21
  obj.fetch(arg)
21
- rescue ArgumentError, IndexError => e
22
- break block.call(arg) if block
23
- raise UndefinedPathError, "Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace
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
24
26
  end
25
27
  end
26
28
  end
@@ -0,0 +1,69 @@
1
+ require 'hashie/extensions/deep_locate'
2
+ module Hashie
3
+ module Extensions
4
+ module DeepFind
5
+ # Performs a depth-first search on deeply nested data structures for
6
+ # a key and returns the first occurrence of the key.
7
+ #
8
+ # options = {user: {location: {address: '123 Street'}}}
9
+ # options.extend(Hashie::Extensions::DeepFind)
10
+ # options.deep_find(:address) # => '123 Street'
11
+ #
12
+ # class MyHash < Hash
13
+ # include Hashie::Extensions::DeepFind
14
+ # end
15
+ #
16
+ # my_hash = MyHash.new
17
+ # my_hash[:user] = {location: {address: '123 Street'}}
18
+ # my_hash.deep_find(:address) # => '123 Street'
19
+ def deep_find(key)
20
+ _deep_find(key)
21
+ end
22
+
23
+ alias deep_detect deep_find
24
+
25
+ # Performs a depth-first search on deeply nested data structures for
26
+ # a key and returns all occurrences of the key.
27
+ #
28
+ # options = {
29
+ # users: [
30
+ # { location: {address: '123 Street'} },
31
+ # { location: {address: '234 Street'}}
32
+ # ]
33
+ # }
34
+ # options.extend(Hashie::Extensions::DeepFind)
35
+ # options.deep_find_all(:address) # => ['123 Street', '234 Street']
36
+ #
37
+ # class MyHash < Hash
38
+ # include Hashie::Extensions::DeepFind
39
+ # end
40
+ #
41
+ # my_hash = MyHash.new
42
+ # my_hash[:users] = [
43
+ # {location: {address: '123 Street'}},
44
+ # {location: {address: '234 Street'}}
45
+ # ]
46
+ # my_hash.deep_find_all(:address) # => ['123 Street', '234 Street']
47
+ def deep_find_all(key)
48
+ matches = _deep_find_all(key)
49
+ matches.empty? ? nil : matches
50
+ end
51
+
52
+ alias deep_select deep_find_all
53
+
54
+ private
55
+
56
+ def _deep_find(key, object = self)
57
+ _deep_find_all(key, object).first
58
+ end
59
+
60
+ def _deep_find_all(key, object = self, matches = [])
61
+ deep_locate_result = DeepLocate.deep_locate(key, object).tap do |result|
62
+ result.map! { |element| element[key] }
63
+ end
64
+
65
+ matches.concat(deep_locate_result)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,113 @@
1
+ module Hashie
2
+ module Extensions
3
+ module DeepLocate
4
+ # The module level implementation of #deep_locate, incase you do not want
5
+ # to include/extend the base datastructure. For further examples please
6
+ # see #deep_locate.
7
+ #
8
+ # @example
9
+ # books = [
10
+ # {
11
+ # title: "Ruby for beginners",
12
+ # pages: 120
13
+ # },
14
+ # ...
15
+ # ]
16
+ #
17
+ # DeepLocate.deep_locate -> (key, value, object) { key == :title }, books
18
+ # # => [{:title=>"Ruby for beginners", :pages=>120}, ...]
19
+ def self.deep_locate(comparator, object)
20
+ unless comparator.respond_to?(:call)
21
+ comparator = _construct_key_comparator(comparator, object)
22
+ end
23
+
24
+ _deep_locate(comparator, object)
25
+ end
26
+
27
+ # Performs a depth-first search on deeply nested data structures for a
28
+ # given comparator callable and returns each Enumerable, for which the
29
+ # callable returns true for at least one the its elements.
30
+ #
31
+ # @example
32
+ # books = [
33
+ # {
34
+ # title: "Ruby for beginners",
35
+ # pages: 120
36
+ # },
37
+ # {
38
+ # title: "CSS for intermediates",
39
+ # pages: 80
40
+ # },
41
+ # {
42
+ # title: "Collection of ruby books",
43
+ # books: [
44
+ # {
45
+ # title: "Ruby for the rest of us",
46
+ # pages: 576
47
+ # }
48
+ # ]
49
+ # }
50
+ # ]
51
+ #
52
+ # books.extend(Hashie::Extensions::DeepLocate)
53
+ #
54
+ # # for ruby 1.9 leave *no* space between the lambda rocket and the braces
55
+ # # http://ruby-journal.com/becareful-with-space-in-lambda-hash-rocket-syntax-between-ruby-1-dot-9-and-2-dot-0/
56
+ #
57
+ # books.deep_locate -> (key, value, object) { key == :title && value.include?("Ruby") }
58
+ # # => [{:title=>"Ruby for beginners", :pages=>120},
59
+ # # {:title=>"Ruby for the rest of us", :pages=>576}]
60
+ #
61
+ # books.deep_locate -> (key, value, object) { key == :pages && value <= 120 }
62
+ # # => [{:title=>"Ruby for beginners", :pages=>120},
63
+ # # {:title=>"CSS for intermediates", :pages=>80}]
64
+ def deep_locate(comparator)
65
+ Hashie::Extensions::DeepLocate.deep_locate(comparator, self)
66
+ end
67
+
68
+ def self._construct_key_comparator(search_key, object)
69
+ if object.respond_to?(:indifferent_access?) && object.indifferent_access? ||
70
+ activesupport_indifferent?(object)
71
+ search_key = search_key.to_s
72
+ end
73
+
74
+ lambda do |non_callable_object|
75
+ ->(key, _, _) { key == non_callable_object }
76
+ end.call(search_key)
77
+ end
78
+ private_class_method :_construct_key_comparator
79
+
80
+ def self._deep_locate(comparator, object, result = [])
81
+ if object.is_a?(::Enumerable)
82
+ if object.any? { |value| _match_comparator?(value, comparator, object) }
83
+ result.push object
84
+ end
85
+
86
+ (object.respond_to?(:values) ? object.values : object.entries).each do |value|
87
+ _deep_locate(comparator, value, result)
88
+ end
89
+ end
90
+
91
+ result
92
+ end
93
+ private_class_method :_deep_locate
94
+
95
+ def self._match_comparator?(value, comparator, object)
96
+ if object.is_a?(::Hash)
97
+ key, value = value
98
+ else
99
+ key = nil
100
+ end
101
+
102
+ comparator.call(key, value, object)
103
+ end
104
+ private_class_method :_match_comparator?
105
+
106
+ def self.activesupport_indifferent?(object)
107
+ defined?(::ActiveSupport::HashWithIndifferentAccess) &&
108
+ object.is_a?(::ActiveSupport::HashWithIndifferentAccess)
109
+ end
110
+ private_class_method :activesupport_indifferent?
111
+ end
112
+ end
113
+ end