hashie 3.2.0 → 3.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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