hashie 1.2.0 → 2.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.
@@ -14,6 +14,7 @@ module Hashie
14
14
  # * Assignment (<tt>=</tt>): Sets the attribute of the given method name.
15
15
  # * Existence (<tt>?</tt>): Returns true or false depending on whether that key has been set.
16
16
  # * Bang (<tt>!</tt>): Forces the existence of this key, used for deep Mashes. Think of it as "touch" for mashes.
17
+ # * Under Bang (<tt>_</tt>): Like Bang, but returns a new Mash rather than creating a key. Used to test existance in deep Mashes.
17
18
  #
18
19
  # == Basic Example
19
20
  #
@@ -42,7 +43,18 @@ module Hashie
42
43
  # mash.author!.name = "Michael Bleigh"
43
44
  # mash.author # => <Mash name="Michael Bleigh">
44
45
  #
45
- class Mash < Hashie::Hash
46
+ # == Under Bang Example
47
+ #
48
+ # mash = Mash.new
49
+ # mash.author # => nil
50
+ # mash.author_ # => <Mash>
51
+ # mash.author_.name # => nil
52
+ #
53
+ # mash = Mash.new
54
+ # mash.author_.name = "Michael Bleigh" (assigned to temp object)
55
+ # mash.author # => <Mash>
56
+ #
57
+ class Mash < Hash
46
58
  include Hashie::PrettyInspect
47
59
  alias_method :to_s, :inspect
48
60
 
@@ -55,20 +67,18 @@ module Hashie
55
67
  default ? super(default) : super(&blk)
56
68
  end
57
69
 
58
- class << self
59
- alias [] new
60
-
61
- def subkey_class
62
- self
63
- end
64
- end
70
+ class << self; alias [] new; end
65
71
 
66
72
  def id #:nodoc:
67
- key?("id") ? self["id"] : super
73
+ self["id"]
68
74
  end
69
75
 
70
76
  def type #:nodoc:
71
- key?("type") ? self["type"] : super
77
+ self["type"]
78
+ end
79
+
80
+ def object_id #:nodoc:
81
+ self["object_id"]
72
82
  end
73
83
 
74
84
  alias_method :regular_reader, :[]
@@ -97,6 +107,21 @@ module Hashie
97
107
  regular_reader(ck)
98
108
  end
99
109
 
110
+ # This is the under bang method reader, it will return a temporary new Mash
111
+ # if there isn't a value already assigned to the key requested.
112
+ def underbang_reader(key)
113
+ ck = convert_key(key)
114
+ if key?(ck)
115
+ regular_reader(ck)
116
+ else
117
+ self.class.new
118
+ end
119
+ end
120
+
121
+ def fetch(key, default_value = nil)
122
+ self[key] || block_given? && yield(key) || default_value || super(key)
123
+ end
124
+
100
125
  def delete(key)
101
126
  super(convert_key(key))
102
127
  end
@@ -116,20 +141,22 @@ module Hashie
116
141
 
117
142
  # Performs a deep_update on a duplicate of the
118
143
  # current mash.
119
- def deep_merge(other_hash)
120
- dup.deep_update(other_hash)
144
+ def deep_merge(other_hash, &blk)
145
+ dup.deep_update(other_hash, &blk)
121
146
  end
122
147
  alias_method :merge, :deep_merge
123
148
 
124
149
  # Recursively merges this mash with the passed
125
150
  # in hash, merging each hash in the hierarchy.
126
- def deep_update(other_hash)
151
+ def deep_update(other_hash, &blk)
127
152
  other_hash.each_pair do |k,v|
128
153
  key = convert_key(k)
129
154
  if regular_reader(key).is_a?(Mash) and v.is_a?(::Hash)
130
- regular_reader(key).deep_update(v)
155
+ regular_reader(key).deep_update(v, &blk)
131
156
  else
132
- regular_writer(key, convert_value(v, true))
157
+ value = convert_value(v, true)
158
+ value = blk.call(key, self[k], value) if blk
159
+ regular_writer(key, value)
133
160
  end
134
161
  end
135
162
  self
@@ -161,7 +188,7 @@ module Hashie
161
188
 
162
189
  def method_missing(method_name, *args, &blk)
163
190
  return self.[](method_name, &blk) if key?(method_name)
164
- match = method_name.to_s.match(/(.*?)([?=!]?)$/)
191
+ match = method_name.to_s.match(/(.*?)([?=!_]?)$/)
165
192
  case match[2]
166
193
  when "="
167
194
  self[match[1]] = args.first
@@ -169,6 +196,8 @@ module Hashie
169
196
  !!self[match[1]]
170
197
  when "!"
171
198
  initializing_reader(match[1])
199
+ when "_"
200
+ underbang_reader(match[1])
172
201
  else
173
202
  default(method_name, *args, &blk)
174
203
  end
@@ -184,9 +213,11 @@ module Hashie
184
213
  case val
185
214
  when self.class
186
215
  val.dup
216
+ when Hash
217
+ duping ? val.dup : val
187
218
  when ::Hash
188
219
  val = val.dup if duping
189
- self.class.subkey_class.new.merge(val)
220
+ self.class.new(val)
190
221
  when Array
191
222
  val.collect{ |e| convert_value(e) }
192
223
  else
@@ -7,23 +7,39 @@ module Hashie
7
7
  # Trashes are useful when you need to read data from another application,
8
8
  # such as a Java api, where the keys are named differently from how we would
9
9
  # in Ruby.
10
- class Trash < Hashie::Dash
10
+ class Trash < Dash
11
11
 
12
12
  # Defines a property on the Trash. Options are as follows:
13
13
  #
14
14
  # * <tt>:default</tt> - Specify a default value for this property, to be
15
15
  # returned before a value is set on the property in a new Dash.
16
16
  # * <tt>:from</tt> - Specify the original key name that will be write only.
17
+ # * <tt>:with</tt> - Specify a lambda to be used to convert value.
18
+ # * <tt>:transform_with</tt> - Specify a lambda to be used to convert value
19
+ # without using the :from option. It transform the property itself.
17
20
  def self.property(property_name, options = {})
18
21
  super
19
22
 
20
23
  if options[:from]
24
+ if property_name.to_sym == options[:from].to_sym
25
+ raise ArgumentError, "Property name (#{property_name}) and :from option must not be the same"
26
+ end
21
27
  translations << options[:from].to_sym
22
- class_eval <<-RUBY
23
- def #{options[:from]}=(val)
24
- self[:#{property_name}] = val
28
+ if options[:with].respond_to? :call
29
+ class_eval do
30
+ define_method "#{options[:from]}=" do |val|
31
+ self[property_name.to_sym] = options[:with].call(val)
32
+ end
25
33
  end
26
- RUBY
34
+ else
35
+ class_eval <<-RUBY
36
+ def #{options[:from]}=(val)
37
+ self[:#{property_name}] = val
38
+ end
39
+ RUBY
40
+ end
41
+ elsif options[:transform_with].respond_to? :call
42
+ transforms[property_name.to_sym] = options[:transform_with]
27
43
  end
28
44
  end
29
45
 
@@ -32,6 +48,8 @@ module Hashie
32
48
  def []=(property, value)
33
49
  if self.class.translations.include? property.to_sym
34
50
  send("#{property}=", value)
51
+ elsif self.class.transforms.key? property.to_sym
52
+ super property, self.class.transforms[property.to_sym].call(value)
35
53
  elsif property_exists? property
36
54
  super
37
55
  end
@@ -43,6 +61,10 @@ module Hashie
43
61
  @translations ||= []
44
62
  end
45
63
 
64
+ def self.transforms
65
+ @transforms ||= {}
66
+ end
67
+
46
68
  # Raises an NoMethodError if the property doesn't exist
47
69
  #
48
70
  def property_exists?(property)
@@ -51,5 +73,19 @@ module Hashie
51
73
  end
52
74
  true
53
75
  end
76
+
77
+ private
78
+
79
+ # Deletes any keys that have a translation
80
+ def initialize_attributes(attributes)
81
+ return unless attributes
82
+ attributes_copy = attributes.dup.delete_if do |k,v|
83
+ if self.class.translations.include?(k.to_sym)
84
+ self[k] = v
85
+ true
86
+ end
87
+ end
88
+ super attributes_copy
89
+ end
54
90
  end
55
91
  end
@@ -1,3 +1,3 @@
1
1
  module Hashie
2
- VERSION = '1.2.0'
2
+ VERSION = '2.0.0'
3
3
  end
@@ -22,6 +22,14 @@ class Subclassed < DashTest
22
22
  property :last_name, :required => true
23
23
  end
24
24
 
25
+ class DashDefaultTest < Hashie::Dash
26
+ property :aliases, :default => ["Snake"]
27
+ end
28
+
29
+ class DeferredTest < Hashie::Dash
30
+ property :created_at, :default => Proc.new { Time.now }
31
+ end
32
+
25
33
  describe DashTest do
26
34
 
27
35
  subject { DashTest.new(:first_name => 'Bob', :email => 'bob@example.com') }
@@ -96,6 +104,17 @@ describe DashTest do
96
104
  end
97
105
  end
98
106
 
107
+ context 'reading from deferred properties' do
108
+ it 'should evaluate proc after initial read' do
109
+ DeferredTest.new['created_at'].should be_instance_of(Time)
110
+ end
111
+
112
+ it "should not evalute proc after subsequent reads" do
113
+ deferred = DeferredTest.new
114
+ deferred['created_at'].object_id.should == deferred['created_at'].object_id
115
+ end
116
+ end
117
+
99
118
  describe '.new' do
100
119
  it 'fails with non-existent properties' do
101
120
  lambda { described_class.new(:bork => '') }.should raise_error(NoMethodError)
@@ -120,6 +139,12 @@ describe DashTest do
120
139
  expect { DashTest.new }.to raise_error(ArgumentError)
121
140
  end
122
141
 
142
+ it "does not overwrite default values" do
143
+ obj1 = DashDefaultTest.new
144
+ obj1.aliases << "El Rey"
145
+ obj2 = DashDefaultTest.new
146
+ obj2.aliases.should_not include "El Rey"
147
+ end
123
148
  end
124
149
 
125
150
  describe 'properties' do
@@ -0,0 +1,89 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hashie::Extensions::Coercion do
4
+ class Initializable
5
+ def initialize(obj, coerced = false)
6
+ @coerced = coerced
7
+ @value = obj.class.to_s
8
+ end
9
+ def coerced?; @coerced end
10
+ attr_reader :value
11
+ end
12
+
13
+ class Coercable < Initializable
14
+ def self.coerce(obj)
15
+ new(obj, true)
16
+ end
17
+ end
18
+
19
+ before(:each) do
20
+ class ExampleCoercableHash < Hash
21
+ include Hashie::Extensions::Coercion
22
+ include Hashie::Extensions::MergeInitializer
23
+ end
24
+ end
25
+ subject { ExampleCoercableHash }
26
+ let(:instance){ subject.new }
27
+
28
+ describe '.coerce_key' do
29
+ it { subject.should be_respond_to(:coerce_key) }
30
+
31
+ it 'should run through coerce on a specified key' do
32
+ subject.coerce_key :foo, Coercable
33
+
34
+ instance[:foo] = "bar"
35
+ instance[:foo].should be_coerced
36
+ end
37
+
38
+ it "should support an array of keys" do
39
+ subject.coerce_keys :foo, :bar, Coercable
40
+
41
+ instance[:foo] = "bar"
42
+ instance[:bar] = "bax"
43
+ instance[:foo].should be_coerced
44
+ instance[:bar].should be_coerced
45
+ end
46
+
47
+ it 'should just call #new if no coerce method is available' do
48
+ subject.coerce_key :foo, Initializable
49
+
50
+ instance[:foo] = "bar"
51
+ instance[:foo].value.should == "String"
52
+ instance[:foo].should_not be_coerced
53
+ end
54
+
55
+ it "should coerce when the merge initializer is used" do
56
+ subject.coerce_key :foo, Coercable
57
+ instance = subject.new(:foo => "bar")
58
+
59
+ instance[:foo].should be_coerced
60
+ end
61
+ end
62
+
63
+ describe '.coerce_value' do
64
+ context 'with :strict => true' do
65
+ it 'should coerce any value of the exact right class' do
66
+ subject.coerce_value String, Coercable
67
+
68
+ instance[:foo] = "bar"
69
+ instance[:bar] = "bax"
70
+ instance[:foo].should be_coerced
71
+ instance[:bar].should be_coerced
72
+ end
73
+
74
+ it 'should not coerce superclasses' do
75
+ klass = Class.new(String)
76
+ subject.coerce_value klass, Coercable
77
+
78
+ instance[:foo] = "bar"
79
+ instance[:foo].should_not be_kind_of(Coercable)
80
+ instance[:foo] = klass.new
81
+ instance[:foo].should be_kind_of(Coercable)
82
+ end
83
+ end
84
+ end
85
+
86
+ after(:each) do
87
+ Object.send(:remove_const, :ExampleCoercableHash)
88
+ end
89
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hashie::Extensions::DeepMerge do
4
+ class DeepMergeHash < Hash; include Hashie::Extensions::DeepMerge end
5
+
6
+ subject{ DeepMergeHash }
7
+
8
+ let(:h1) { subject.new.merge(:a => "a", :b => "b", :c => { :c1 => "c1", :c2 => "c2", :c3 => { :d1 => "d1" } }) }
9
+ let(:h2) { { :a => 1, :c => { :c1 => 2, :c3 => { :d2 => "d2" } } } }
10
+ let(:expected_hash) { { :a => 1, :b => "b", :c => { :c1 => 2, :c2 => "c2", :c3 => { :d1 => "d1", :d2 => "d2" } } } }
11
+
12
+ it 'should deep merge two hashes' do
13
+ h1.deep_merge(h2).should == expected_hash
14
+ end
15
+
16
+ it 'should deep merge two hashes with bang method' do
17
+ h1.deep_merge!(h2)
18
+ h1.should == expected_hash
19
+ end
20
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hashie::Extensions::IndifferentAccess do
4
+ class IndifferentHash < Hash
5
+ include Hashie::Extensions::MergeInitializer
6
+ include Hashie::Extensions::IndifferentAccess
7
+ end
8
+ subject{ IndifferentHash }
9
+
10
+ it 'should be able to access via string or symbol' do
11
+ h = subject.new(:abc => 123)
12
+ h[:abc].should == 123
13
+ h['abc'].should == 123
14
+ end
15
+
16
+ describe '#values_at' do
17
+ it 'should indifferently find values' do
18
+ h = subject.new(:foo => 'bar', 'baz' => 'qux')
19
+ h.values_at('foo', :baz).should == %w(bar qux)
20
+ end
21
+ end
22
+
23
+ describe '#fetch' do
24
+ it 'should work like normal fetch, but indifferent' do
25
+ h = subject.new(:foo => 'bar')
26
+ h.fetch(:foo).should == h.fetch('foo')
27
+ h.fetch(:foo).should == 'bar'
28
+ end
29
+ end
30
+
31
+ describe '#delete' do
32
+ it 'should delete indifferently' do
33
+ h = subject.new(:foo => 'bar', 'baz' => 'qux')
34
+ h.delete('foo')
35
+ h.delete(:baz)
36
+ h.should be_empty
37
+ end
38
+ end
39
+
40
+ describe '#key?' do
41
+ let(:h) { subject.new(:foo => 'bar') }
42
+
43
+ it 'should find it indifferently' do
44
+ h.should be_key(:foo)
45
+ h.should be_key('foo')
46
+ end
47
+
48
+ %w(include? member? has_key?).each do |key_alias|
49
+ it "should be aliased as #{key_alias}" do
50
+ h.send(key_alias.to_sym, :foo).should be(true)
51
+ h.send(key_alias.to_sym, 'foo').should be(true)
52
+ end
53
+ end
54
+ end
55
+
56
+ describe '#update' do
57
+ subject{ IndifferentHash.new(:foo => 'bar') }
58
+ it 'should allow keys to be indifferent still' do
59
+ subject.update(:baz => 'qux')
60
+ subject['foo'].should == 'bar'
61
+ subject['baz'].should == 'qux'
62
+ end
63
+
64
+ it 'should recursively inject indifference into sub-hashes' do
65
+ subject.update(:baz => {:qux => 'abc'})
66
+ subject['baz']['qux'].should == 'abc'
67
+ end
68
+
69
+ it 'should not change the ancestors of the injected object class' do
70
+ subject.update(:baz => {:qux => 'abc'})
71
+ Hash.new.should_not be_respond_to(:indifferent_access?)
72
+ end
73
+ end
74
+ end