hashie 3.2.0 → 3.3.1

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.
@@ -1,35 +1,75 @@
1
1
  module Hashie
2
+ class CoercionError < StandardError; end
3
+
2
4
  module Extensions
3
5
  module Coercion
6
+ CORE_TYPES = {
7
+ Integer => :to_i,
8
+ Float => :to_f,
9
+ Complex => :to_c,
10
+ Rational => :to_r,
11
+ String => :to_s,
12
+ Symbol => :to_sym
13
+ }
14
+
15
+ ABSTRACT_CORE_TYPES = {
16
+ Integer => [Fixnum, Bignum],
17
+ Numeric => [Fixnum, Bignum, Float, Complex, Rational]
18
+ }
19
+
4
20
  def self.included(base)
5
21
  base.send :include, InstanceMethods
6
22
  base.extend ClassMethods # NOTE: we wanna make sure we first define set_value_with_coercion before extending
7
23
 
8
- base.send :alias_method, :'set_value_without_coercion', :[]=
9
- base.send :alias_method, :[]=, :'set_value_with_coercion'
24
+ base.send :alias_method, :set_value_without_coercion, :[]= unless base.method_defined?(:set_value_without_coercion)
25
+ base.send :alias_method, :[]=, :set_value_with_coercion
10
26
  end
11
27
 
12
28
  module InstanceMethods
13
29
  def set_value_with_coercion(key, value)
14
30
  into = self.class.key_coercion(key) || self.class.value_coercion(value)
15
31
 
16
- return set_value_without_coercion(key, value) unless value && into
17
- return set_value_without_coercion(key, coerce_or_init(into).call(value)) unless into.is_a?(Enumerable)
32
+ return set_value_without_coercion(key, value) if value.nil? || into.nil?
18
33
 
19
- if into.class >= Hash
20
- key_coerce = coerce_or_init(into.flatten[0])
21
- value_coerce = coerce_or_init(into.flatten[-1])
22
- value = Hash[value.map { |k, v| [key_coerce.call(k), value_coerce.call(v)] }]
23
- else # Enumerable but not Hash: Array, Set
24
- value_coerce = coerce_or_init(into.first)
25
- value = into.class.new(value.map { |v| value_coerce.call(v) })
34
+ begin
35
+ return set_value_without_coercion(key, coerce_or_init(into).call(value)) unless into.is_a?(Enumerable)
36
+
37
+ if into.class >= Hash
38
+ key_coerce = coerce_or_init(into.flatten[0])
39
+ value_coerce = coerce_or_init(into.flatten[-1])
40
+ value = Hash[value.map { |k, v| [key_coerce.call(k), value_coerce.call(v)] }]
41
+ else # Enumerable but not Hash: Array, Set
42
+ value_coerce = coerce_or_init(into.first)
43
+ value = into.class.new(value.map { |v| value_coerce.call(v) })
44
+ end
45
+ rescue NoMethodError, TypeError => e
46
+ raise CoercionError, "Cannot coerce property #{key.inspect} from #{value.class} to #{into}: #{e.message}"
26
47
  end
27
48
 
28
49
  set_value_without_coercion(key, value)
29
50
  end
30
51
 
31
52
  def coerce_or_init(type)
32
- type.respond_to?(:coerce) ? ->(v) { type.coerce(v) } : ->(v) { type.new(v) }
53
+ return type if type.is_a? Proc
54
+
55
+ if CORE_TYPES.key?(type)
56
+ lambda do |v|
57
+ return v if v.is_a? type
58
+ return v.send(CORE_TYPES[type])
59
+ end
60
+ elsif type.respond_to?(:coerce)
61
+ lambda do |v|
62
+ return v if v.is_a? type
63
+ type.coerce(v)
64
+ end
65
+ elsif type.respond_to?(:new)
66
+ lambda do |v|
67
+ return v if v.is_a? type
68
+ type.new(v)
69
+ end
70
+ else
71
+ fail TypeError, "#{type} is not a coercable type"
72
+ end
33
73
  end
34
74
 
35
75
  private :coerce_or_init
@@ -102,6 +142,12 @@ module Hashie
102
142
  def coerce_value(from, into, options = {})
103
143
  options = { strict: true }.merge(options)
104
144
 
145
+ if ABSTRACT_CORE_TYPES.key? from
146
+ ABSTRACT_CORE_TYPES[from].each do | type |
147
+ coerce_value type, into, options
148
+ end
149
+ end
150
+
105
151
  if options[:strict]
106
152
  (@strict_value_coercions ||= {})[from] = into
107
153
  else
@@ -0,0 +1,59 @@
1
+ module Hashie
2
+ module Extensions
3
+ module DeepFind
4
+ # Performs a depth-first search on deeply nested data structures for
5
+ # a key and returns the first occurrence of the key.
6
+ #
7
+ # options = {user: {location: {address: '123 Street'}}}
8
+ # options.deep_find(:address) # => '123 Street'
9
+ def deep_find(key)
10
+ _deep_find(key)
11
+ end
12
+
13
+ alias_method :deep_detect, :deep_find
14
+
15
+ # Performs a depth-first search on deeply nested data structures for
16
+ # a key and returns all occurrences of the key.
17
+ #
18
+ # options = {users: [{location: {address: '123 Street'}}, {location: {address: '234 Street'}}]}
19
+ # options.deep_find_all(:address) # => ['123 Street', '234 Street']
20
+ def deep_find_all(key)
21
+ matches = _deep_find_all(key)
22
+ matches.empty? ? nil : matches
23
+ end
24
+
25
+ alias_method :deep_select, :deep_find_all
26
+
27
+ private
28
+
29
+ def _deep_find(key, object = self)
30
+ if object.respond_to?(:key?)
31
+ return object[key] if object.key?(key)
32
+
33
+ reduce_to_match(key, object.values)
34
+ elsif object.is_a?(Enumerable)
35
+ reduce_to_match(key, object)
36
+ end
37
+ end
38
+
39
+ def _deep_find_all(key, object = self, matches = [])
40
+ if object.respond_to?(:key?)
41
+ matches << object[key] if object.key?(key)
42
+ object.values.each { |v| _deep_find_all(key, v, matches) }
43
+ elsif object.is_a?(Enumerable)
44
+ object.each { |v| _deep_find_all(key, v, matches) }
45
+ end
46
+
47
+ matches
48
+ end
49
+
50
+ def reduce_to_match(key, enumerable)
51
+ enumerable.reduce(nil) do |found, value|
52
+ return found if found
53
+
54
+ _deep_find(key, value)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -83,9 +83,9 @@ module Hashie
83
83
 
84
84
  def convert_value(value)
85
85
  if hash_lacking_indifference?(value)
86
- IndifferentAccess.inject(value.dup)
86
+ IndifferentAccess.inject!(value)
87
87
  elsif value.is_a?(::Array)
88
- value.dup.replace(value.map { |e| convert_value(e) })
88
+ value.replace(value.map { |e| convert_value(e) })
89
89
  else
90
90
  value
91
91
  end
@@ -0,0 +1,13 @@
1
+ module Hashie
2
+ module Extensions
3
+ module Mash
4
+ module SafeAssignment
5
+ def assign_property(name, value)
6
+ fail ArgumentError, "The property #{name} clashes with an existing method." if methods.include?(name.to_sym)
7
+
8
+ self[name] = value
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -120,5 +120,80 @@ module Hashie
120
120
  end
121
121
  end
122
122
  end
123
+
124
+ # MethodOverridingWriter gives you #key_name= shortcuts for
125
+ # writing to your hash. It allows methods to be overridden by
126
+ # #key_name= shortcuts and aliases those methods with two
127
+ # leading underscores.
128
+ #
129
+ # Keys are written as strings. Override #convert_key if you
130
+ # would like to have symbols or something else.
131
+ #
132
+ # Note that MethodOverridingWriter also overrides
133
+ # #respond_to_missing? such that any #method_name= will respond
134
+ # appropriately as true.
135
+ #
136
+ # @example
137
+ # class MyHash < Hash
138
+ # include Hashie::Extensions::MethodOverridingWriter
139
+ # end
140
+ #
141
+ # h = MyHash.new
142
+ # h.awesome = 'sauce'
143
+ # h['awesome'] # => 'sauce'
144
+ # h.zip = 'a-dee-doo-dah'
145
+ # h.zip # => 'a-dee-doo-dah'
146
+ # h.__zip # => [[['awesome', 'sauce'], ['zip', 'a-dee-doo-dah']]]
147
+ #
148
+ module MethodOverridingWriter
149
+ def convert_key(key)
150
+ key.to_s
151
+ end
152
+
153
+ def method_missing(name, *args)
154
+ if args.size == 1 && name.to_s =~ /(.*)=$/
155
+ key = Regexp.last_match[1]
156
+ redefine_method(key) if method?(key) && !already_overridden?(key)
157
+ return self[convert_key(key)] = args.first
158
+ end
159
+
160
+ super
161
+ end
162
+
163
+ def respond_to_missing?(name, include_private = false)
164
+ return true if name.to_s.end_with?('=')
165
+ super
166
+ end
167
+
168
+ protected
169
+
170
+ def already_overridden?(name)
171
+ method?("__#{name}")
172
+ end
173
+
174
+ def method?(name)
175
+ methods.map { |m| m.to_s }.include?(name)
176
+ end
177
+
178
+ def redefine_method(method_name)
179
+ eigenclass = class << self; self; end
180
+ eigenclass.__send__(:alias_method, "__#{method_name}", method_name)
181
+ eigenclass.__send__(:define_method, method_name, -> { self[method_name] })
182
+ end
183
+ end
184
+
185
+ # A macro module that will automatically include MethodReader,
186
+ # MethodOverridingWriter, and MethodQuery, giving you the ability
187
+ # to read, write, and query keys in a hash using method call
188
+ # shortcuts that can override object methods. Any overridden
189
+ # object method is automatically aliased with two leading
190
+ # underscores.
191
+ module MethodAccessWithOverride
192
+ def self.included(base)
193
+ [MethodReader, MethodOverridingWriter, MethodQuery].each do |mod|
194
+ base.send :include, mod
195
+ end
196
+ end
197
+ end
123
198
  end
124
199
  end
@@ -0,0 +1,21 @@
1
+ require 'yaml'
2
+ require 'erb'
3
+ module Hashie
4
+ module Extensions
5
+ module Parsers
6
+ class YamlErbParser
7
+ def initialize(file_path)
8
+ @content = File.read(file_path)
9
+ end
10
+
11
+ def perform
12
+ YAML.load ERB.new(@content).result
13
+ end
14
+
15
+ def self.perform(file_path)
16
+ new(file_path).perform
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
data/lib/hashie/mash.rb CHANGED
@@ -59,6 +59,25 @@ module Hashie
59
59
 
60
60
  ALLOWED_SUFFIXES = %w(? ! = _)
61
61
 
62
+ def self.load(path, options = {})
63
+ @_mashes ||= new do |h, file_path|
64
+ fail ArgumentError, "The following file doesn't exist: #{file_path}" unless File.file?(file_path)
65
+
66
+ parser = options.fetch(:parser) { Hashie::Extensions::Parsers::YamlErbParser }
67
+ h[file_path] = new(parser.perform(file_path)).freeze
68
+ end
69
+ @_mashes[path]
70
+ end
71
+
72
+ def to_module(mash_method_name = :settings)
73
+ mash = self
74
+ Module.new do |m|
75
+ m.send :define_method, mash_method_name.to_sym do
76
+ mash
77
+ end
78
+ end
79
+ end
80
+
62
81
  alias_method :to_s, :inspect
63
82
 
64
83
  # If you pass in an existing hash, it will
@@ -171,6 +190,11 @@ module Hashie
171
190
  alias_method :update, :deep_update
172
191
  alias_method :merge!, :update
173
192
 
193
+ # Assigns a value to a key
194
+ def assign_property(name, value)
195
+ self[name] = value
196
+ end
197
+
174
198
  # Performs a shallow_update on a duplicate of the current mash
175
199
  def shallow_merge(other_hash)
176
200
  dup.shallow_update(other_hash)
@@ -212,7 +236,7 @@ module Hashie
212
236
  name, suffix = method_suffix(method_name)
213
237
  case suffix
214
238
  when '='
215
- self[name] = args.first
239
+ assign_property(name, args.first)
216
240
  when '?'
217
241
  !!self[name]
218
242
  when '!'
data/lib/hashie/rash.rb CHANGED
@@ -60,6 +60,28 @@ module Hashie
60
60
  all(key).first
61
61
  end
62
62
 
63
+ #
64
+ # Raise (or yield) unless something matches the key.
65
+ #
66
+ def fetch(*args)
67
+ fail ArgumentError, "Expected 1-2 arguments, got #{args.length}" \
68
+ unless (1..2).cover?(args.length)
69
+
70
+ key, default = args
71
+
72
+ all(key) do |value|
73
+ return value
74
+ end
75
+
76
+ if block_given?
77
+ yield key
78
+ elsif default
79
+ default
80
+ else
81
+ fail KeyError, "key not found: #{key.inspect}"
82
+ end
83
+ end
84
+
63
85
  #
64
86
  # Return everything that matches the query.
65
87
  #
@@ -106,6 +128,10 @@ module Hashie
106
128
  @hash.send(*args, &block)
107
129
  end
108
130
 
131
+ def respond_to_missing?(*args)
132
+ @hash.respond_to?(*args)
133
+ end
134
+
109
135
  private
110
136
 
111
137
  def optimize_if_necessary!
data/lib/hashie/trash.rb CHANGED
@@ -26,11 +26,13 @@ module Hashie
26
26
  fail ArgumentError, "Property name (#{property_name}) and :from option must not be the same"
27
27
  end
28
28
 
29
- translations[options[:from]] = property_name
29
+ translations_hash[options[:from]] ||= {}
30
+ translations_hash[options[:from]][property_name] = options[:with] || options[:transform_with]
30
31
 
31
32
  define_method "#{options[:from]}=" do |val|
32
- with = options[:with] || options[:transform_with]
33
- self[property_name] = with.respond_to?(:call) ? with.call(val) : val
33
+ self.class.translations_hash[options[:from]].each do |name, with|
34
+ self[name] = with.respond_to?(:call) ? with.call(val) : val
35
+ end
34
36
  end
35
37
  else
36
38
  if options[:transform_with].respond_to? :call
@@ -39,6 +41,18 @@ module Hashie
39
41
  end
40
42
  end
41
43
 
44
+ class << self
45
+ attr_reader :transforms, :translations_hash
46
+ end
47
+ instance_variable_set('@transforms', {})
48
+ instance_variable_set('@translations_hash', {})
49
+
50
+ def self.inherited(klass)
51
+ super
52
+ klass.instance_variable_set('@transforms', transforms.dup)
53
+ klass.instance_variable_set('@translations_hash', translations_hash.dup)
54
+ end
55
+
42
56
  # Set a value on the Dash in a Hash-like way. Only works
43
57
  # on pre-existing properties.
44
58
  def []=(property, value)
@@ -56,7 +70,7 @@ module Hashie
56
70
  end
57
71
 
58
72
  def self.translation_exists?(name)
59
- translations.key? name
73
+ translations_hash.key? name
60
74
  end
61
75
 
62
76
  def self.transformation_exists?(name)
@@ -69,20 +83,26 @@ module Hashie
69
83
 
70
84
  private
71
85
 
72
- def self.properties
73
- @properties ||= []
74
- end
75
-
76
86
  def self.translations
77
- @translations ||= {}
87
+ @translations ||= begin
88
+ h = {}
89
+ translations_hash.each do |(property_name, property_translations)|
90
+ h[property_name] = property_translations.size > 1 ? property_translations.keys : property_translations.keys.first
91
+ end
92
+ h
93
+ end
78
94
  end
79
95
 
80
96
  def self.inverse_translations
81
- @inverse_translations ||= Hash[translations.map(&:reverse)]
82
- end
83
-
84
- def self.transforms
85
- @transforms ||= {}
97
+ @inverse_translations ||= begin
98
+ h = {}
99
+ translations_hash.each do |(property_name, property_translations)|
100
+ property_translations.keys.each do |k|
101
+ h[k] = property_name
102
+ end
103
+ end
104
+ h
105
+ end
86
106
  end
87
107
 
88
108
  # Raises an NoMethodError if the property doesn't exist
@@ -98,7 +118,7 @@ module Hashie
98
118
  def initialize_attributes(attributes)
99
119
  return unless attributes
100
120
  attributes_copy = attributes.dup.delete_if do |k, v|
101
- if self.class.translations.include?(k)
121
+ if self.class.translations_hash.include?(k)
102
122
  self[k] = v
103
123
  true
104
124
  end