hashie 3.6.0 → 4.0.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +95 -7
  4. data/UPGRADING.md +78 -2
  5. data/hashie.gemspec +2 -1
  6. data/lib/hashie.rb +20 -19
  7. data/lib/hashie/dash.rb +2 -1
  8. data/lib/hashie/extensions/active_support/core_ext/hash.rb +14 -0
  9. data/lib/hashie/extensions/coercion.rb +23 -16
  10. data/lib/hashie/extensions/dash/indifferent_access.rb +20 -1
  11. data/lib/hashie/extensions/dash/property_translation.rb +5 -2
  12. data/lib/hashie/extensions/deep_fetch.rb +4 -2
  13. data/lib/hashie/extensions/deep_find.rb +12 -3
  14. data/lib/hashie/extensions/deep_locate.rb +22 -7
  15. data/lib/hashie/extensions/indifferent_access.rb +1 -3
  16. data/lib/hashie/extensions/key_conflict_warning.rb +55 -0
  17. data/lib/hashie/extensions/mash/define_accessors.rb +90 -0
  18. data/lib/hashie/extensions/mash/keep_original_keys.rb +2 -1
  19. data/lib/hashie/extensions/mash/safe_assignment.rb +3 -1
  20. data/lib/hashie/extensions/method_access.rb +5 -2
  21. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +9 -4
  22. data/lib/hashie/extensions/strict_key_access.rb +8 -4
  23. data/lib/hashie/hash.rb +16 -9
  24. data/lib/hashie/mash.rb +99 -43
  25. data/lib/hashie/railtie.rb +7 -0
  26. data/lib/hashie/rash.rb +1 -1
  27. data/lib/hashie/version.rb +1 -1
  28. data/spec/hashie/dash_spec.rb +18 -8
  29. data/spec/hashie/extensions/coercion_spec.rb +17 -8
  30. data/spec/hashie/extensions/deep_find_spec.rb +12 -6
  31. data/spec/hashie/extensions/deep_locate_spec.rb +2 -1
  32. data/spec/hashie/extensions/deep_merge_spec.rb +6 -2
  33. data/spec/hashie/extensions/ignore_undeclared_spec.rb +2 -1
  34. data/spec/hashie/extensions/mash/define_accessors_spec.rb +90 -0
  35. data/spec/hashie/extensions/method_access_spec.rb +8 -1
  36. data/spec/hashie/extensions/strict_key_access_spec.rb +9 -10
  37. data/spec/hashie/extensions/symbolize_keys_spec.rb +3 -1
  38. data/spec/hashie/hash_spec.rb +45 -6
  39. data/spec/hashie/mash_spec.rb +314 -8
  40. data/spec/hashie/trash_spec.rb +9 -3
  41. data/spec/integration/elasticsearch/integration_spec.rb +3 -2
  42. data/spec/integration/rails/app.rb +5 -12
  43. data/spec/integration/rails/integration_spec.rb +22 -1
  44. metadata +8 -4
@@ -7,11 +7,23 @@ module Hashie
7
7
  base.send :include, Hashie::Extensions::IndifferentAccess
8
8
  end
9
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
+
10
22
  module ClassMethods
11
23
  # Check to see if the specified property has already been
12
24
  # defined.
13
25
  def property?(name)
14
- name = translations[name.to_sym] if included_modules.include?(Hashie::Extensions::Dash::PropertyTranslation) && translation_exists?(name)
26
+ name = translations[name.to_sym] if translation_for?(name)
15
27
  name = name.to_s
16
28
  !!properties.find { |property| property.to_s == name }
17
29
  end
@@ -30,6 +42,13 @@ module Hashie
30
42
  name = name.to_s
31
43
  !!transforms.keys.find { |key| key.to_s == name }
32
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
33
52
  end
34
53
  end
35
54
  end
@@ -58,7 +58,9 @@ module Hashie
58
58
  end
59
59
 
60
60
  def permitted_input_keys
61
- @permitted_input_keys ||= properties.map { |property| inverse_translations.fetch property, property }
61
+ @permitted_input_keys ||=
62
+ properties
63
+ .map { |property| inverse_translations.fetch property, property }
62
64
  end
63
65
 
64
66
  # Defines a property on the Trash. Options are as follows:
@@ -135,7 +137,8 @@ module Hashie
135
137
  end
136
138
 
137
139
  def fail_self_transformation_error!(property_name)
138
- raise ArgumentError, "Property name (#{property_name}) and :from option must not be the same"
140
+ raise ArgumentError,
141
+ "Property name (#{property_name}) and :from option must not be the same"
139
142
  end
140
143
 
141
144
  def valid_transformer?(transformer)
@@ -9,7 +9,8 @@ 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
 
@@ -20,7 +21,8 @@ module Hashie
20
21
  obj.fetch(arg)
21
22
  rescue ArgumentError, IndexError, NoMethodError => e
22
23
  break yield(arg) if block
23
- raise UndefinedPathError, "Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace
24
+ raise UndefinedPathError,
25
+ "Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace
24
26
  end
25
27
  end
26
28
  end
@@ -1,3 +1,4 @@
1
+ require 'hashie/extensions/deep_locate'
1
2
  module Hashie
2
3
  module Extensions
3
4
  module DeepFind
@@ -24,7 +25,12 @@ module Hashie
24
25
  # Performs a depth-first search on deeply nested data structures for
25
26
  # a key and returns all occurrences of the key.
26
27
  #
27
- # options = {users: [{location: {address: '123 Street'}}, {location: {address: '234 Street'}}]}
28
+ # options = {
29
+ # users: [
30
+ # { location: {address: '123 Street'} },
31
+ # { location: {address: '234 Street'}}
32
+ # ]
33
+ # }
28
34
  # options.extend(Hashie::Extensions::DeepFind)
29
35
  # options.deep_find_all(:address) # => ['123 Street', '234 Street']
30
36
  #
@@ -33,7 +39,10 @@ module Hashie
33
39
  # end
34
40
  #
35
41
  # my_hash = MyHash.new
36
- # my_hash[:users] = [{location: {address: '123 Street'}}, {location: {address: '234 Street'}}]
42
+ # my_hash[:users] = [
43
+ # {location: {address: '123 Street'}},
44
+ # {location: {address: '234 Street'}}
45
+ # ]
37
46
  # my_hash.deep_find_all(:address) # => ['123 Street', '234 Street']
38
47
  def deep_find_all(key)
39
48
  matches = _deep_find_all(key)
@@ -49,7 +58,7 @@ module Hashie
49
58
  end
50
59
 
51
60
  def _deep_find_all(key, object = self, matches = [])
52
- deep_locate_result = Hashie::Extensions::DeepLocate.deep_locate(key, object).tap do |result|
61
+ deep_locate_result = DeepLocate.deep_locate(key, object).tap do |result|
53
62
  result.map! { |element| element[key] }
54
63
  end
55
64
 
@@ -14,10 +14,12 @@ module Hashie
14
14
  # ...
15
15
  # ]
16
16
  #
17
- # Hashie::Extensions::DeepLocate.deep_locate -> (key, value, object) { key == :title }, books
17
+ # DeepLocate.deep_locate -> (key, value, object) { key == :title }, books
18
18
  # # => [{:title=>"Ruby for beginners", :pages=>120}, ...]
19
19
  def self.deep_locate(comparator, object)
20
- comparator = _construct_key_comparator(comparator, object) unless comparator.respond_to?(:call)
20
+ unless comparator.respond_to?(:call)
21
+ comparator = _construct_key_comparator(comparator, object)
22
+ end
21
23
 
22
24
  _deep_locate(comparator, object)
23
25
  end
@@ -53,17 +55,21 @@ module Hashie
53
55
  # # http://ruby-journal.com/becareful-with-space-in-lambda-hash-rocket-syntax-between-ruby-1-dot-9-and-2-dot-0/
54
56
  #
55
57
  # books.deep_locate -> (key, value, object) { key == :title && value.include?("Ruby") }
56
- # # => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"Ruby for the rest of us", :pages=>576}]
58
+ # # => [{:title=>"Ruby for beginners", :pages=>120},
59
+ # # {:title=>"Ruby for the rest of us", :pages=>576}]
57
60
  #
58
61
  # books.deep_locate -> (key, value, object) { key == :pages && value <= 120 }
59
- # # => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"CSS for intermediates", :pages=>80}]
62
+ # # => [{:title=>"Ruby for beginners", :pages=>120},
63
+ # # {:title=>"CSS for intermediates", :pages=>80}]
60
64
  def deep_locate(comparator)
61
65
  Hashie::Extensions::DeepLocate.deep_locate(comparator, self)
62
66
  end
63
67
 
64
68
  def self._construct_key_comparator(search_key, object)
65
- search_key = search_key.to_s if defined?(::ActiveSupport::HashWithIndifferentAccess) && object.is_a?(::ActiveSupport::HashWithIndifferentAccess)
66
- search_key = search_key.to_s if object.respond_to?(:indifferent_access?) && object.indifferent_access?
69
+ if object.respond_to?(:indifferent_access?) && object.indifferent_access? ||
70
+ activesupport_indifferent?(object)
71
+ search_key = search_key.to_s
72
+ end
67
73
 
68
74
  lambda do |non_callable_object|
69
75
  ->(key, _, _) { key == non_callable_object }
@@ -73,7 +79,10 @@ module Hashie
73
79
 
74
80
  def self._deep_locate(comparator, object, result = [])
75
81
  if object.is_a?(::Enumerable)
76
- result.push object if object.any? { |value| _match_comparator?(value, comparator, object) }
82
+ if object.any? { |value| _match_comparator?(value, comparator, object) }
83
+ result.push object
84
+ end
85
+
77
86
  (object.respond_to?(:values) ? object.values : object.entries).each do |value|
78
87
  _deep_locate(comparator, value, result)
79
88
  end
@@ -93,6 +102,12 @@ module Hashie
93
102
  comparator.call(key, value, object)
94
103
  end
95
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?
96
111
  end
97
112
  end
98
113
  end
@@ -24,9 +24,7 @@ module Hashie
24
24
  #
25
25
  module IndifferentAccess
26
26
  def self.included(base)
27
- Hashie::Extensions::Dash::IndifferentAccess::ClassMethods.tap do |extension|
28
- base.extend(extension) if base <= Hashie::Dash && !base.singleton_class.included_modules.include?(extension)
29
- end
27
+ Hashie::Extensions::Dash::IndifferentAccess.maybe_extend(base)
30
28
 
31
29
  base.class_eval do
32
30
  alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
@@ -0,0 +1,55 @@
1
+ module Hashie
2
+ module Extensions
3
+ module KeyConflictWarning
4
+ class CannotDisableMashWarnings < StandardError
5
+ def initialize
6
+ super(
7
+ 'You cannot disable warnings on the base Mash class. ' \
8
+ 'Please subclass the Mash and disable it in the subclass.'
9
+ )
10
+ end
11
+ end
12
+
13
+ # Disable the logging of warnings based on keys conflicting keys/methods
14
+ #
15
+ # @api semipublic
16
+ # @return [void]
17
+ def disable_warnings(*method_keys)
18
+ raise CannotDisableMashWarnings if self == Hashie::Mash
19
+ if method_keys.any?
20
+ disabled_warnings.concat(method_keys).tap(&:flatten!).uniq!
21
+ else
22
+ disabled_warnings.clear
23
+ end
24
+
25
+ @disable_warnings = true
26
+ end
27
+
28
+ # Checks whether this class disables warnings for conflicting keys/methods
29
+ #
30
+ # @api semipublic
31
+ # @return [Boolean]
32
+ def disable_warnings?(method_key = nil)
33
+ return disabled_warnings.include?(method_key) if disabled_warnings.any? && method_key
34
+ @disable_warnings ||= false
35
+ end
36
+
37
+ # Returns an array of methods that this class disables warnings for.
38
+ #
39
+ # @api semipublic
40
+ # @return [Boolean]
41
+ def disabled_warnings
42
+ @_disabled_warnings ||= []
43
+ end
44
+
45
+ # Inheritance hook that sets class configuration when inherited.
46
+ #
47
+ # @api semipublic
48
+ # @return [void]
49
+ def inherited(subclass)
50
+ super
51
+ subclass.disable_warnings(disabled_warnings) if disable_warnings?
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,90 @@
1
+ module Hashie
2
+ module Extensions
3
+ module Mash
4
+ module DefineAccessors
5
+ def self.included(klass)
6
+ klass.class_eval do
7
+ mod = Ext.new
8
+ include mod
9
+ end
10
+ end
11
+
12
+ def self.extended(obj)
13
+ included(obj.singleton_class)
14
+ end
15
+
16
+ class Ext < Module
17
+ def initialize
18
+ mod = self
19
+ define_method(:method_missing) do |method_name, *args, &block|
20
+ key, suffix = method_name_and_suffix(method_name)
21
+ case suffix
22
+ when '='.freeze
23
+ mod.define_writer(key, method_name)
24
+ when '?'.freeze
25
+ mod.define_predicate(key, method_name)
26
+ when '!'.freeze
27
+ mod.define_initializing_reader(key, method_name)
28
+ when '_'.freeze
29
+ mod.define_underbang_reader(key, method_name)
30
+ else
31
+ mod.define_reader(key, method_name)
32
+ end
33
+ send(method_name, *args, &block)
34
+ end
35
+ end
36
+
37
+ def define_reader(key, method_name)
38
+ define_method(method_name) do |&block|
39
+ if key? method_name
40
+ self.[](method_name, &block)
41
+ else
42
+ self.[](key, &block)
43
+ end
44
+ end
45
+ end
46
+
47
+ def define_writer(key, method_name)
48
+ define_method(method_name) do |value = nil|
49
+ if key? method_name
50
+ self.[](method_name, &proc)
51
+ else
52
+ assign_property(key, value)
53
+ end
54
+ end
55
+ end
56
+
57
+ def define_predicate(key, method_name)
58
+ define_method(method_name) do
59
+ if key? method_name
60
+ self.[](method_name, &proc)
61
+ else
62
+ !!self[key]
63
+ end
64
+ end
65
+ end
66
+
67
+ def define_initializing_reader(key, method_name)
68
+ define_method(method_name) do
69
+ if key? method_name
70
+ self.[](method_name, &proc)
71
+ else
72
+ initializing_reader(key)
73
+ end
74
+ end
75
+ end
76
+
77
+ def define_underbang_reader(key, method_name)
78
+ define_method(method_name) do
79
+ if key? method_name
80
+ self.[](key, &proc)
81
+ else
82
+ underbang_reader(key)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -15,7 +15,8 @@ module Hashie
15
15
  # mash[:symbol_key] == mash['symbol_key'] #=> true
16
16
  module KeepOriginalKeys
17
17
  def self.included(descendant)
18
- raise ArgumentError, "#{descendant} is not a kind of Hashie::Mash" unless descendant <= Hashie::Mash
18
+ error_message = "#{descendant} is not a kind of Hashie::Mash"
19
+ raise ArgumentError, error_message unless descendant <= Hashie::Mash
19
20
  end
20
21
 
21
22
  private
@@ -3,7 +3,9 @@ module Hashie
3
3
  module Mash
4
4
  module SafeAssignment
5
5
  def custom_writer(key, *args) #:nodoc:
6
- raise ArgumentError, "The property #{key} clashes with an existing method." if !key?(key) && respond_to?(key, true)
6
+ if !key?(key) && respond_to?(key, true)
7
+ raise ArgumentError, "The property #{key} clashes with an existing method."
8
+ end
7
9
  super
8
10
  end
9
11
 
@@ -73,7 +73,9 @@ module Hashie
73
73
  end
74
74
 
75
75
  def method_missing(name, *args)
76
- return self[convert_key(Regexp.last_match[1])] = args.first if args.size == 1 && name.to_s =~ /(.*)=$/
76
+ if args.size == 1 && name.to_s =~ /(.*)=$/
77
+ return self[convert_key(Regexp.last_match[1])] = args.first
78
+ end
77
79
 
78
80
  super
79
81
  end
@@ -231,7 +233,8 @@ module Hashie
231
233
  # underscores.
232
234
  module MethodAccessWithOverride
233
235
  def self.included(base)
234
- [MethodReader, MethodOverridingWriter, MethodQuery, MethodOverridingInitializer].each do |mod|
236
+ [MethodReader, MethodOverridingWriter,
237
+ MethodQuery, MethodOverridingInitializer].each do |mod|
235
238
  base.send :include, mod
236
239
  end
237
240
  end
@@ -6,19 +6,24 @@ module Hashie
6
6
  module Extensions
7
7
  module Parsers
8
8
  class YamlErbParser
9
- def initialize(file_path)
9
+ def initialize(file_path, options = {})
10
10
  @content = File.read(file_path)
11
11
  @file_path = file_path.is_a?(Pathname) ? file_path.to_s : file_path
12
+ @options = options
12
13
  end
13
14
 
14
15
  def perform
15
16
  template = ERB.new(@content)
16
17
  template.filename = @file_path
17
- YAML.safe_load template.result
18
+ permitted_classes = @options.fetch(:permitted_classes) { [] }
19
+ permitted_symbols = @options.fetch(:permitted_symbols) { [] }
20
+ aliases = @options.fetch(:aliases) { true }
21
+ # TODO: Psych in newer rubies expects these args to be keyword args.
22
+ YAML.safe_load template.result, permitted_classes, permitted_symbols, aliases
18
23
  end
19
24
 
20
- def self.perform(file_path)
21
- new(file_path).perform
25
+ def self.perform(file_path, options = {})
26
+ new(file_path, options).perform
22
27
  end
23
28
  end
24
29
  end
@@ -1,6 +1,7 @@
1
1
  module Hashie
2
2
  module Extensions
3
- # SRP: This extension will fail an error whenever a key is accessed that does not exist in the hash.
3
+ # SRP: This extension will fail an error whenever a key is accessed
4
+ # that does not exist in the hash.
4
5
  #
5
6
  # EXAMPLE:
6
7
  #
@@ -15,12 +16,15 @@ module Hashie
15
16
  # >> hash[:cow]
16
17
  # KeyError: key not found: :cow
17
18
  #
18
- # NOTE: For googlers coming from Python to Ruby, this extension makes a Hash behave more like a "Dictionary".
19
+ # NOTE: For googlers coming from Python to Ruby, this extension makes a Hash
20
+ # behave more like a "Dictionary".
19
21
  #
20
22
  module StrictKeyAccess
21
23
  class DefaultError < StandardError
22
- def initialize(msg = 'Setting or using a default with Hashie::Extensions::StrictKeyAccess does not make sense', *args)
23
- super
24
+ def initialize
25
+ super('Setting or using a default with Hashie::Extensions::StrictKeyAccess'\
26
+ ' does not make sense'
27
+ )
24
28
  end
25
29
  end
26
30
 
@@ -18,20 +18,21 @@ module Hashie
18
18
  def to_hash(options = {})
19
19
  out = {}
20
20
  each_key do |k|
21
- assignment_key = if options[:stringify_keys]
22
- k.to_s
23
- elsif options[:symbolize_keys]
24
- k.to_s.to_sym
25
- else
26
- k
27
- end
21
+ assignment_key =
22
+ if options[:stringify_keys]
23
+ k.to_s
24
+ elsif options[:symbolize_keys]
25
+ k.to_s.to_sym
26
+ else
27
+ k
28
+ end
28
29
  if self[k].is_a?(Array)
29
30
  out[assignment_key] ||= []
30
31
  self[k].each do |array_object|
31
- out[assignment_key] << (array_object.is_a?(Hash) ? flexibly_convert_to_hash(array_object, options) : array_object)
32
+ out[assignment_key] << maybe_convert_to_hash(array_object, options)
32
33
  end
33
34
  else
34
- out[assignment_key] = self[k].is_a?(Hash) || self[k].respond_to?(:to_hash) ? flexibly_convert_to_hash(self[k], options) : self[k]
35
+ out[assignment_key] = maybe_convert_to_hash(self[k], options)
35
36
  end
36
37
  end
37
38
  out
@@ -44,6 +45,12 @@ module Hashie
44
45
 
45
46
  private
46
47
 
48
+ def maybe_convert_to_hash(object, options)
49
+ return object unless object.is_a?(Hash) || object.respond_to?(:to_hash)
50
+
51
+ flexibly_convert_to_hash(object, options)
52
+ end
53
+
47
54
  def flexibly_convert_to_hash(object, options = {})
48
55
  if object.method(:to_hash).arity.zero?
49
56
  object.to_hash